diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md new file mode 100644 index 0000000000..ed9c7719a8 --- /dev/null +++ b/CHANGELOG-WIP.md @@ -0,0 +1,49 @@ +# Release Notes for Craft Commerce 5.4 (WIP) + +### Store Management +- It’s now possible to set variants’ statuses from Edit Product screens. ([#3953](https://github.com/craftcms/commerce/discussions/3953)) +- Coupon validation errors now provide an explanation when invalid due to order, customer, or address conditions. ([#3935](https://github.com/craftcms/commerce/issues/3935)) +- Catalog Pricing Rule screens now display custom metadata. ([#3975](https://github.com/craftcms/commerce/pull/3975)) +- Product indexes now have a “Promotional Price” column. + +### Administration +- It’s now possible to set a variants’ default “Promotable”, “Track Inventory”, “Allow out of stock purchases”, and “Available for purchase” values when configuring variant field layouts. ([#3571](https://github.com/craftcms/commerce/issues/3571)) +- Shipping methods and shipping rules now support flexible customer matching, based on a customer condition. ([#3925](https://github.com/craftcms/commerce/issues/3925)) +- Gateways now have a “Match Order” condition that determines which orders the gateway should be available for. ([#3913](https://github.com/craftcms/commerce/discussions/3913)) +- All native order attributes are now available as card attributes. ([#4019](https://github.com/craftcms/commerce/issues/4019)) +- Added the `resave/variants` command. + +### Development +- Line item snapshots now contain a `catalogPricingRuleId` field. ([#3910](https://github.com/craftcms/commerce/issues/3910)) +- Added the `localized` field to products’ GraphQL data. ([#3783](https://github.com/craftcms/commerce/discussions/3783)) + +### Extensibility +- Added `craft\commerce\base\Gateway::getConfig()`. +- Added `craft\commerce\base\Gateway::getOrderCondition()`. +- Added `craft\commerce\base\Gateway::getOrderCondition()`. +- Added `craft\commerce\base\Gateway::hasOrderCondition()`. +- Added `craft\commerce\base\Gateway::setOrderCondition()`. +- Added `craft\commerce\base\Gateway::setOrderCondition()`. +- Added `craft\commerce\base\Gateway::setOrderCondition()`. +- Added `craft\commerce\base\Purchasable::$catalogPricingRuleId`. +- Added `craft\commerce\base\Purchasable::getCatalogPricingRule()`. +- Added `craft\commerce\controllers\OrdersController::actionCopyAddressToUser()`. +- Added `craft\commerce\elements\Product::$defaultBasePromotionalPrice` +- Added `craft\commerce\elements\conditions\customers\ShippingMethodCustomerCondition`. +- Added `craft\commerce\elements\conditions\customers\ShippingRuleCustomerCondition`. +- Added `craft\commerce\models\ShippingMethod::getCustomerCondition()`. +- Added `craft\commerce\models\ShippingMethod::setCustomerCondition()`. +- Added `craft\commerce\models\ShippingRule::getCustomerCondition()`. +- Added `craft\commerce\models\ShippingRule::setCustomerCondition()`. +- Added `craft\commerce\taxidvalidators\EuVatIdValidator::API_URL`. + +### System +- Improved store query performance. ([#4029](https://github.com/craftcms/commerce/issues/4029)) +- Fixed a bug where the purchasable cache was not cleared when stock was updated. +- Fixed a PHP error that could occur when sending emails. ([#4017](https://github.com/craftcms/commerce/issues/4017)) +- Fixed a SQL error that could occur when upgrading to Commerce 5. ([#4044](https://github.com/craftcms/commerce/issues/4044)) +- Fixed a bug where duplicate order references could be generated. ([#4050](https://github.com/craftcms/commerce/issues/4050)) +- Fixed a bug where purchasables’ `shippingCategoryId` and `taxCategoryId` properties couldn’t be set via `setAttributes()`. ([#4046](https://github.com/craftcms/commerce/issues/4046)) +- Fixed a bug where gateway settings weren’t storing project config values consistently. ([#3941](https://github.com/craftcms/commerce/issues/3941)) +- Fixed a bug where new line items did not expose their submitted quantity to the `craft\commerce\services\LineItems::EVENT_POPULATE_LINE_ITEM` event. ([#3883](https://github.com/craftcms/commerce/issues/3883)) +- Fixed a PHP error that could occur when saving an order. ([#3283](https://github.com/craftcms/commerce/issues/3283)) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c7c98ed3..0d2e8049ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,5 @@ # Release Notes for Craft Commerce -## Unreleased - -- Improved store query performance. ([#4029](https://github.com/craftcms/commerce/issues/4029)) -- Fixed a bug where purchasable cache was not cleared when stock was updated. -- Fixed a PHP error that could occur when sending emails. ([#4017](https://github.com/craftcms/commerce/issues/4017)) -- Fixed a SQL error that could occur when upgrading to Commerce 5. ([#4044](https://github.com/craftcms/commerce/issues/4044)) -- Fixed a bug where duplicate order references could be generated. ([#4050](https://github.com/craftcms/commerce/issues/4050)) -- Fixed a bug where `shippingCategoryId` and `taxCategoryId` were not marked as Purchasable attributes. ([#4046](https://github.com/craftcms/commerce/issues/4046)) - ## 5.3.13 - 2025-05-21 - Fixed a bug where the “Recipient”, “BCC’d Recipient”, and “CC’d Recipient” email settings weren’t working properly if set to environment variables. ([#4004](https://github.com/craftcms/commerce/issues/4004), [#4002](https://github.com/craftcms/commerce/issues/4002)) diff --git a/composer.lock b/composer.lock index ca12f99341..fc02233879 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -80,7 +80,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -110,7 +110,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -118,7 +118,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "cebe/markdown", @@ -331,16 +331,16 @@ }, { "name": "craftcms/cms", - "version": "5.6.4", + "version": "5.7.8", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "7547163b18b33b3ccde54ea97713a95fd2e94f14" + "reference": "3fea28125abcf67e0f556da9f1ee8093d58e3641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/7547163b18b33b3ccde54ea97713a95fd2e94f14", - "reference": "7547163b18b33b3ccde54ea97713a95fd2e94f14", + "url": "https://api.github.com/repos/craftcms/cms/zipball/3fea28125abcf67e0f556da9f1ee8093d58e3641", + "reference": "3fea28125abcf67e0f556da9f1ee8093d58e3641", "shasum": "" }, "require": { @@ -378,34 +378,31 @@ "symfony/css-selector": "^6.0|^7.0", "symfony/dom-crawler": "^6.0|^7.0", "symfony/filesystem": "^6.3", - "symfony/http-client": "^6.0.3", + "symfony/http-client": "^6.0.3|^7.0", "symfony/property-access": "^7.0", "symfony/property-info": "^7.0", "symfony/serializer": "^6.4", "symfony/var-dumper": "^5.0|^6.0", - "symfony/yaml": "^5.2.3", + "symfony/yaml": "^5.2.3|^6.0", "theiconic/name-parser": "^1.2", "twig/twig": "~3.15.0", "voku/stringy": "^6.4.0", "web-auth/webauthn-lib": "~4.9.0", - "webonyx/graphql-php": "~14.11.5", - "yiisoft/yii2": "~2.0.51.0", - "yiisoft/yii2-debug": "~2.1.25.0", + "webonyx/graphql-php": "~14.11.10", + "yiisoft/yii2": "~2.0.52.0", + "yiisoft/yii2-debug": "~2.1.26.0", "yiisoft/yii2-queue": "~2.3.2", "yiisoft/yii2-symfonymailer": "^4.0.0" }, - "conflict": { - "webonyx/graphql-php": "14.11.7" - }, "provide": { "bower-asset/inputmask": "5.0.9", "bower-asset/jquery": "3.6.1", - "bower-asset/punycode": "2.3.1", - "bower-asset/yii2-pjax": "2.0.8", + "bower-asset/punycode": "^1.4", + "bower-asset/yii2-pjax": "~2.0.1", "yii2tech/ar-softdelete": "1.0.4" }, "require-dev": { - "codeception/codeception": "^5.0.11", + "codeception/codeception": "^5.2.0", "codeception/lib-innerbrowser": "4.0.1", "codeception/module-asserts": "^3.0.0", "codeception/module-datafactory": "^3.0.0", @@ -416,6 +413,7 @@ "fakerphp/faker": "^1.19.0", "league/factory-muffin": "^3.3.0", "phpstan/phpstan": "^1.10.56", + "rector/rector": "^1.2", "vlucas/phpdotenv": "^5.4.1", "yiisoft/yii2-redis": "^2.0" }, @@ -456,7 +454,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2025-01-31T00:08:51+00:00" + "time": "2025-05-28T18:48:09+00:00" }, { "name": "craftcms/plugin-installer", @@ -513,16 +511,16 @@ }, { "name": "craftcms/server-check", - "version": "5.0.2", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/craftcms/server-check.git", - "reference": "2c0578a3b0e663402ce5bf752e7308218937fad9" + "reference": "08082638f8caff8ab86a223898e8ea167b3f5879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/server-check/zipball/2c0578a3b0e663402ce5bf752e7308218937fad9", - "reference": "2c0578a3b0e663402ce5bf752e7308218937fad9", + "url": "https://api.github.com/repos/craftcms/server-check/zipball/08082638f8caff8ab86a223898e8ea167b3f5879", + "reference": "08082638f8caff8ab86a223898e8ea167b3f5879", "shasum": "" }, "type": "library", @@ -551,7 +549,7 @@ "rss": "https://github.com/craftcms/server-check/releases.atom", "source": "https://github.com/craftcms/server-check" }, - "time": "2024-09-16T15:18:27+00:00" + "time": "2025-02-11T20:26:29+00:00" }, { "name": "creocoder/yii2-nested-sets", @@ -716,29 +714,29 @@ }, { "name": "doctrine/collections", - "version": "2.2.2", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", "shasum": "" }, "require": { "doctrine/deprecations": "^1", - "php": "^8.1" + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { "doctrine/coding-standard": "^12", "ext-json": "*", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.11" + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { @@ -782,7 +780,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.2.2" + "source": "https://github.com/doctrine/collections/tree/2.3.0" }, "funding": [ { @@ -798,30 +796,33 @@ "type": "tidelift" } ], - "time": "2024-04-18T06:56:21+00:00" + "time": "2025-03-22T10:17:19+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -841,9 +842,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/lexer", @@ -986,16 +987,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -1041,7 +1042,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -1049,7 +1050,7 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "elvanto/litemoji", @@ -1202,16 +1203,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -1308,7 +1309,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, "funding": [ { @@ -1324,20 +1325,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { @@ -1391,7 +1392,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.2.0" }, "funding": [ { @@ -1407,20 +1408,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -1507,7 +1508,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, "funding": [ { @@ -1523,7 +1524,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "ibericode/vat", @@ -1636,7 +1637,7 @@ }, { "name": "illuminate/collections", - "version": "v10.48.26", + "version": "v10.48.28", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -1691,7 +1692,7 @@ }, { "name": "illuminate/conditionable", - "version": "v10.48.26", + "version": "v10.48.28", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -1737,7 +1738,7 @@ }, { "name": "illuminate/contracts", - "version": "v10.48.26", + "version": "v10.48.28", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -1785,7 +1786,7 @@ }, { "name": "illuminate/macroable", - "version": "v10.48.26", + "version": "v10.48.28", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2182,16 +2183,16 @@ }, { "name": "moneyphp/money", - "version": "v4.6.0", + "version": "v4.7.0", "source": { "type": "git", "url": "https://github.com/moneyphp/money.git", - "reference": "ddf6a86b574808f8844777ed4e8c4f92a10dac9b" + "reference": "af048f0206d3b39b8fad9de6a230cedf765365fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/moneyphp/money/zipball/ddf6a86b574808f8844777ed4e8c4f92a10dac9b", - "reference": "ddf6a86b574808f8844777ed4e8c4f92a10dac9b", + "url": "https://api.github.com/repos/moneyphp/money/zipball/af048f0206d3b39b8fad9de6a230cedf765365fa", + "reference": "af048f0206d3b39b8fad9de6a230cedf765365fa", "shasum": "" }, "require": { @@ -2213,10 +2214,12 @@ "php-http/message": "^1.16.0", "php-http/mock-client": "^1.6.0", "phpbench/phpbench": "^1.2.5", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.9", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^10.5.9", - "psalm/plugin-phpunit": "^0.18.4", "psr/cache": "^1.0.1 || ^2.0 || ^3.0", - "vimeo/psalm": "~5.20.0" + "ticketswap/phpstan-error-formatter": "^1.1" }, "suggest": { "ext-gmp": "Calculate without integer limits", @@ -2264,22 +2267,22 @@ ], "support": { "issues": "https://github.com/moneyphp/money/issues", - "source": "https://github.com/moneyphp/money/tree/v4.6.0" + "source": "https://github.com/moneyphp/money/tree/v4.7.0" }, - "time": "2024-11-22T10:59:03+00:00" + "time": "2025-04-03T08:26:36+00:00" }, { "name": "monolog/monolog", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { @@ -2357,7 +2360,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -2369,7 +2372,7 @@ "type": "tidelift" } ], - "time": "2024-12-05T17:15:07+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -2633,16 +2636,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.1", + "version": "5.6.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", "shasum": "" }, "require": { @@ -2691,9 +2694,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" }, - "time": "2024-12-07T09:39:29+00:00" + "time": "2025-04-13T19:20:35+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2755,16 +2758,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { @@ -2796,9 +2799,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-10-13T11:29:49+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "pixelandtonic/imagine", @@ -3506,16 +3509,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v8.7.0", + "version": "v8.8.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf" + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/f414ff953002a9b18e3a116f5e462c56f21237cf", - "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/3de493bdddfd1f051249af725c7e0d2c38fed740", + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740", "shasum": "" }, "require": { @@ -3523,7 +3526,7 @@ "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.40" + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -3565,9 +3568,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.7.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.8.0" }, - "time": "2024-10-27T17:38:32+00:00" + "time": "2025-03-23T17:59:05+00:00" }, { "name": "samdark/yii2-psr-log-target", @@ -3688,31 +3691,31 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.2", + "version": "v2.6.3", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "9e013b376939c0d4029f54150d2a16f3c67a5797" + "reference": "67c31f5e50c93c20579ca9e23035d8c540b51941" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/9e013b376939c0d4029f54150d2a16f3c67a5797", - "reference": "9e013b376939c0d4029f54150d2a16f3c67a5797", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/67c31f5e50c93c20579ca9e23035d8c540b51941", + "reference": "67c31f5e50c93c20579ca9e23035d8c540b51941", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^5.6 || ^7.0 || ^8.0" + "php": "^7.1 || ^8.0" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "~5.7", + "phpunit/phpunit": "^7", "setasign/fpdf": "~1.8.6", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", - "tecnickcom/tcpdf": "~6.2" + "tecnickcom/tcpdf": "^6.2" }, "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." @@ -3748,7 +3751,7 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.2" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.3" }, "funding": [ { @@ -3756,7 +3759,7 @@ "type": "tidelift" } ], - "time": "2024-12-10T13:12:19+00:00" + "time": "2025-02-05T13:22:35+00:00" }, { "name": "spomky-labs/cbor-php", @@ -3843,20 +3846,20 @@ }, { "name": "spomky-labs/pki-framework", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/pki-framework.git", - "reference": "5ac374c3e295c8b917208ff41b4d30f76668478c" + "reference": "5ff1dcc21e961b60149a80e77f744fc047800b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/5ac374c3e295c8b917208ff41b4d30f76668478c", - "reference": "5ac374c3e295c8b917208ff41b4d30f76668478c", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/5ff1dcc21e961b60149a80e77f744fc047800b31", + "reference": "5ff1dcc21e961b60149a80e77f744fc047800b31", "shasum": "" }, "require": { - "brick/math": "^0.10|^0.11|^0.12", + "brick/math": "^0.10|^0.11|^0.12|^0.13", "ext-mbstring": "*", "php": ">=8.1" }, @@ -3871,7 +3874,7 @@ "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", "phpstan/phpstan-phpunit": "^1.1|^2.0", "phpstan/phpstan-strict-rules": "^1.3|^2.0", - "phpunit/phpunit": "^10.1|^11.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", "symfony/string": "^6.4|^7.0", @@ -3936,7 +3939,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/pki-framework/issues", - "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.2" + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.3" }, "funding": [ { @@ -3948,11 +3951,11 @@ "type": "patreon" } ], - "time": "2025-01-03T09:35:48+00:00" + "time": "2025-04-25T15:57:13+00:00" }, { "name": "symfony/css-selector", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -3997,7 +4000,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" }, "funding": [ { @@ -4017,16 +4020,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -4039,7 +4042,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4064,7 +4067,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -4080,20 +4083,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/dom-crawler", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "700a880e5089280c7cf3ca1ccf9d9de6630f5d25" + "reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/700a880e5089280c7cf3ca1ccf9d9de6630f5d25", - "reference": "700a880e5089280c7cf3ca1ccf9d9de6630f5d25", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0fabbc3d6a9c473b716a93fc8e7a537adb396166", + "reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166", "shasum": "" }, "require": { @@ -4131,7 +4134,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.2.3" + "source": "https://github.com/symfony/dom-crawler/tree/v7.3.0" }, "funding": [ { @@ -4147,20 +4150,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-03-05T10:15:41+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", "shasum": "" }, "require": { @@ -4211,7 +4214,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" }, "funding": [ { @@ -4227,20 +4230,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-04-22T09:11:45+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -4254,7 +4257,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4287,7 +4290,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -4303,7 +4306,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", @@ -4373,28 +4376,29 @@ }, { "name": "symfony/http-client", - "version": "v6.4.18", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "394b440934056b8d9d6ba250001458e9d7998b7f" + "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/394b440934056b8d9d6ba250001458e9d7998b7f", - "reference": "394b440934056b8d9d6ba250001458e9d7998b7f", + "url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9", + "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { + "amphp/amp": "<2.5", "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" + "symfony/http-foundation": "<6.4" }, "provide": { "php-http/async-client-implementation": "*", @@ -4403,19 +4407,20 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -4446,7 +4451,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.18" + "source": "https://github.com/symfony/http-client/tree/v7.3.0" }, "funding": [ { @@ -4462,20 +4467,20 @@ "type": "tidelift" } ], - "time": "2025-01-28T15:49:13+00:00" + "time": "2025-05-02T08:23:16+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -4488,7 +4493,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4524,7 +4529,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -4540,20 +4545,20 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:49:48+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/mailer", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" + "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", + "url": "https://api.github.com/repos/symfony/mailer/zipball/0f375bbbde96ae8c78e4aa3e63aabd486e33364c", + "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c", "shasum": "" }, "require": { @@ -4604,7 +4609,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.3" + "source": "https://github.com/symfony/mailer/tree/v7.3.0" }, "funding": [ { @@ -4620,20 +4625,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-04-04T09:51:09+00:00" }, { "name": "symfony/mime", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", + "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", "shasum": "" }, "require": { @@ -4688,7 +4693,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.3" + "source": "https://github.com/symfony/mime/tree/v7.3.0" }, "funding": [ { @@ -4704,11 +4709,11 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-02-19T08:51:26+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4767,7 +4772,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -4787,16 +4792,16 @@ }, { "name": "symfony/polyfill-iconv", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa", + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa", "shasum": "" }, "require": { @@ -4847,7 +4852,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.32.0" }, "funding": [ { @@ -4863,11 +4868,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-17T14:58:18+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -4925,7 +4930,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -4945,16 +4950,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -5008,7 +5013,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { @@ -5024,11 +5029,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5089,7 +5094,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -5109,19 +5114,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -5169,7 +5175,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -5185,7 +5191,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php72", @@ -5254,7 +5260,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -5310,7 +5316,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -5328,9 +5334,85 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, { "name": "symfony/polyfill-uuid", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -5389,7 +5471,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" }, "funding": [ { @@ -5409,16 +5491,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "shasum": "" }, "require": { @@ -5450,7 +5532,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.3.0" }, "funding": [ { @@ -5466,20 +5548,20 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-04-17T09:11:12+00:00" }, { "name": "symfony/property-access", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "b28732e315d81fbec787f838034de7d6c9b2b902" + "reference": "3bcf43665d6aff90547b005348e1e351f4e2174b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/b28732e315d81fbec787f838034de7d6c9b2b902", - "reference": "b28732e315d81fbec787f838034de7d6c9b2b902", + "url": "https://api.github.com/repos/symfony/property-access/zipball/3bcf43665d6aff90547b005348e1e351f4e2174b", + "reference": "3bcf43665d6aff90547b005348e1e351f4e2174b", "shasum": "" }, "require": { @@ -5526,7 +5608,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.2.3" + "source": "https://github.com/symfony/property-access/tree/v7.3.0" }, "funding": [ { @@ -5542,24 +5624,25 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-05-10T11:59:09+00:00" }, { "name": "symfony/property-info", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "dedb118fd588a92f226b390250b384d25f4192fe" + "reference": "200d230d8553610ada73ac557501dc4609aad31f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/dedb118fd588a92f226b390250b384d25f4192fe", - "reference": "dedb118fd588a92f226b390250b384d25f4192fe", + "url": "https://api.github.com/repos/symfony/property-info/zipball/200d230d8553610ada73ac557501dc4609aad31f", + "reference": "200d230d8553610ada73ac557501dc4609aad31f", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/string": "^6.4|^7.0", "symfony/type-info": "~7.1.9|^7.2.2" }, @@ -5611,7 +5694,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.2.3" + "source": "https://github.com/symfony/property-info/tree/v7.3.0" }, "funding": [ { @@ -5627,20 +5710,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-04-04T13:12:05+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.18", + "version": "v6.4.22", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3" + "reference": "b836df93e9ea07d1d3ada58a679ef205d54b64d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/6ad986f62276da4c8c69754decfaa445a89cb6e3", - "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3", + "url": "https://api.github.com/repos/symfony/serializer/zipball/b836df93e9ea07d1d3ada58a679ef205d54b64d1", + "reference": "b836df93e9ea07d1d3ada58a679ef205d54b64d1", "shasum": "" }, "require": { @@ -5709,7 +5792,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.18" + "source": "https://github.com/symfony/serializer/tree/v6.4.22" }, "funding": [ { @@ -5725,20 +5808,20 @@ "type": "tidelift" } ], - "time": "2025-01-28T18:47:02+00:00" + "time": "2025-05-12T08:02:50+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -5756,7 +5839,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5792,7 +5875,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -5808,20 +5891,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { @@ -5879,7 +5962,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -5895,28 +5978,32 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-04-20T20:19:01+00:00" }, { "name": "symfony/type-info", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "3b5a17470fff0034f25fd4287cbdaa0010d2f749" + "reference": "bc9af22e25796d98078f69c0749ab3a9d3454786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/3b5a17470fff0034f25fd4287cbdaa0010d2f749", - "reference": "3b5a17470fff0034f25fd4287cbdaa0010d2f749", + "url": "https://api.github.com/repos/symfony/type-info/zipball/bc9af22e25796d98078f69c0749ab3a9d3454786", + "reference": "bc9af22e25796d98078f69c0749ab3a9d3454786", "shasum": "" }, "require": { "php": ">=8.2", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" }, "require-dev": { - "phpstan/phpdoc-parser": "^1.0|^2.0" + "phpstan/phpdoc-parser": "^1.30|^2.0" }, "type": "library", "autoload": { @@ -5954,7 +6041,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.2" + "source": "https://github.com/symfony/type-info/tree/v7.3.0" }, "funding": [ { @@ -5970,20 +6057,20 @@ "type": "tidelift" } ], - "time": "2024-12-20T13:38:37+00:00" + "time": "2025-03-30T12:17:06+00:00" }, { "name": "symfony/uid", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "url": "https://api.github.com/repos/symfony/uid/zipball/7beeb2b885cd584cd01e126c5777206ae4c3c6a3", + "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3", "shasum": "" }, "require": { @@ -6028,7 +6115,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.2.0" + "source": "https://github.com/symfony/uid/tree/v7.3.0" }, "funding": [ { @@ -6044,20 +6131,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-05-24T14:28:13+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.18", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", "shasum": "" }, "require": { @@ -6113,7 +6200,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" }, "funding": [ { @@ -6129,35 +6216,32 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:26:11+00:00" + "time": "2025-04-09T07:34:50+00:00" }, { "name": "symfony/yaml", - "version": "v5.4.45", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e" + "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e", + "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.3" + "symfony/console": "<5.4" }, "require-dev": { - "symfony/console": "^5.3|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "symfony/console": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -6188,7 +6272,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.45" + "source": "https://github.com/symfony/yaml/tree/v6.4.21" }, "funding": [ { @@ -6204,20 +6288,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-04-04T09:48:44+00:00" }, { "name": "tecnickcom/tcpdf", - "version": "6.8.2", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "f7a781073e1645062f163e058139e2f89355d420" + "reference": "ca5b6de294512145db96bcbc94e61696599c391d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/f7a781073e1645062f163e058139e2f89355d420", - "reference": "f7a781073e1645062f163e058139e2f89355d420", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", + "reference": "ca5b6de294512145db96bcbc94e61696599c391d", "shasum": "" }, "require": { @@ -6230,8 +6314,6 @@ "config", "include", "tcpdf.php", - "tcpdf_parser.php", - "tcpdf_import.php", "tcpdf_barcodes_1d.php", "tcpdf_barcodes_2d.php", "include/tcpdf_colors.php", @@ -6269,15 +6351,15 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.8.2" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", "type": "custom" } ], - "time": "2025-01-26T14:03:12+00:00" + "time": "2025-05-27T18:02:28+00:00" }, { "name": "theiconic/name-parser", @@ -7346,29 +7428,28 @@ }, { "name": "yiisoft/yii2", - "version": "2.0.51", + "version": "2.0.52", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "ea1f112f4dc9a9824e77b788019e2d53325d823c" + "reference": "540e7387d934c52e415614aa081fb38d04c72d9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/ea1f112f4dc9a9824e77b788019e2d53325d823c", - "reference": "ea1f112f4dc9a9824e77b788019e2d53325d823c", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/540e7387d934c52e415614aa081fb38d04c72d9a", + "reference": "540e7387d934c52e415614aa081fb38d04c72d9a", "shasum": "" }, "require": { "bower-asset/inputmask": "^5.0.8 ", "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", - "bower-asset/punycode": "^2.2", + "bower-asset/punycode": "^1.4", "bower-asset/yii2-pjax": "~2.0.1", "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", "ext-ctype": "*", "ext-mbstring": "*", "ezyang/htmlpurifier": "^4.17", "lib-pcre": "*", - "paragonie/random_compat": ">=1", "php": ">=7.3.0", "yiisoft/yii2-composer": "~2.0.4" }, @@ -7464,20 +7545,20 @@ "type": "tidelift" } ], - "time": "2024-07-18T19:50:00+00:00" + "time": "2025-02-13T20:02:28+00:00" }, { "name": "yiisoft/yii2-composer", - "version": "2.0.10", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/b684b01ecb119c8287721def726a0e24fec2fef2", + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2", "shasum": "" }, "require": { @@ -7520,11 +7601,11 @@ "yii2" ], "support": { - "forum": "http://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", "issues": "https://github.com/yiisoft/yii2-composer/issues", "source": "https://github.com/yiisoft/yii2-composer", - "wiki": "http://www.yiiframework.com/wiki/" + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { @@ -7540,20 +7621,20 @@ "type": "tidelift" } ], - "time": "2020-06-24T00:04:01+00:00" + "time": "2025-02-13T20:59:36+00:00" }, { "name": "yiisoft/yii2-debug", - "version": "2.1.25", + "version": "2.1.26", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-debug.git", - "reference": "4d011b9bfc83bde71cde43c9f6837f5a74685ea7" + "reference": "e4b28a1d295fc977d8399db544336dd5b2764397" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/4d011b9bfc83bde71cde43c9f6837f5a74685ea7", - "reference": "4d011b9bfc83bde71cde43c9f6837f5a74685ea7", + "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/e4b28a1d295fc977d8399db544336dd5b2764397", + "reference": "e4b28a1d295fc977d8399db544336dd5b2764397", "shasum": "" }, "require": { @@ -7607,14 +7688,15 @@ "keywords": [ "debug", "debugger", + "dev", "yii2" ], "support": { - "forum": "http://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", "issues": "https://github.com/yiisoft/yii2-debug/issues", "source": "https://github.com/yiisoft/yii2-debug", - "wiki": "http://www.yiiframework.com/wiki/" + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { @@ -7630,7 +7712,7 @@ "type": "tidelift" } ], - "time": "2023-09-26T15:50:00+00:00" + "time": "2025-02-13T21:27:29+00:00" }, { "name": "yiisoft/yii2-queue", @@ -7838,24 +7920,30 @@ "packages-dev": [ { "name": "behat/gherkin", - "version": "v4.11.0", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "32821a17b12620951e755b5d49328a6421a5b5b5" + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/32821a17b12620951e755b5d49328a6421a5b5b5", - "reference": "32821a17b12620951e755b5d49328a6421a5b5b5", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", "shasum": "" }, "require": { + "composer-runtime-api": "^2.2", "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-24.1.0", - "phpunit/phpunit": "^9.6", + "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1", + "friendsofphp/php-cs-fixer": "^3.65", + "mikey179/vfsstream": "^1.6", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^10.5", "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { @@ -7868,8 +7956,8 @@ } }, "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" + "psr-4": { + "Behat\\Gherkin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7880,11 +7968,11 @@ { "name": "Konstantin Kudryashov", "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "homepage": "https://everzet.com" } ], "description": "Gherkin DSL parser for PHP", - "homepage": "http://behat.org/", + "homepage": "https://behat.org/", "keywords": [ "BDD", "Behat", @@ -7895,45 +7983,45 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.11.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.14.0" }, - "time": "2024-12-06T10:07:25+00:00" + "time": "2025-05-23T15:06:40+00:00" }, { "name": "codeception/codeception", - "version": "5.1.2", + "version": "5.3.2", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374" + "reference": "582112d7a603d575e41638df1e96900b10ae91b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374", - "reference": "3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/582112d7a603d575e41638df1e96900b10ae91b8", + "reference": "582112d7a603d575e41638df1e96900b10ae91b8", "shasum": "" }, "require": { - "behat/gherkin": "^4.6.2", - "codeception/lib-asserts": "^2.0", + "behat/gherkin": "^4.12", + "codeception/lib-asserts": "^2.2", "codeception/stub": "^4.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "php": "^8.0", - "phpunit/php-code-coverage": "^9.2 || ^10.0 || ^11.0", - "phpunit/php-text-template": "^2.0 || ^3.0 || ^4.0", - "phpunit/php-timer": "^5.0.3 || ^6.0 || ^7.0", - "phpunit/phpunit": "^9.5.20 || ^10.0 || ^11.0", - "psy/psysh": "^0.11.2 || ^0.12", - "sebastian/comparator": "^4.0.5 || ^5.0 || ^6.0", - "sebastian/diff": "^4.0.3 || ^5.0 || ^6.0", - "symfony/console": ">=4.4.24 <8.0", - "symfony/css-selector": ">=4.4.24 <8.0", - "symfony/event-dispatcher": ">=4.4.24 <8.0", - "symfony/finder": ">=4.4.24 <8.0", - "symfony/var-dumper": ">=4.4.24 <8.0", - "symfony/yaml": ">=4.4.24 <8.0" + "php": "^8.2", + "phpunit/php-code-coverage": "^9.2 | ^10.0 | ^11.0 | ^12.0", + "phpunit/php-text-template": "^2.0 | ^3.0 | ^4.0 | ^5.0", + "phpunit/php-timer": "^5.0.3 | ^6.0 | ^7.0 | ^8.0", + "phpunit/phpunit": "^9.5.20 | ^10.0 | ^11.0 | ^12.0", + "psy/psysh": "^0.11.2 | ^0.12", + "sebastian/comparator": "^4.0.5 | ^5.0 | ^6.0 | ^7.0", + "sebastian/diff": "^4.0.3 | ^5.0 | ^6.0 | ^7.0", + "symfony/console": ">=5.4.24 <8.0", + "symfony/css-selector": ">=5.4.24 <8.0", + "symfony/event-dispatcher": ">=5.4.24 <8.0", + "symfony/finder": ">=5.4.24 <8.0", + "symfony/var-dumper": ">=5.4.24 <8.0", + "symfony/yaml": ">=5.4.24 <8.0" }, "conflict": { "codeception/lib-innerbrowser": "<3.1.3", @@ -7945,17 +8033,23 @@ }, "require-dev": { "codeception/lib-innerbrowser": "*@dev", - "codeception/lib-web": "^1.0", + "codeception/lib-web": "*@dev", "codeception/module-asserts": "*@dev", "codeception/module-cli": "*@dev", "codeception/module-db": "*@dev", "codeception/module-filesystem": "*@dev", "codeception/module-phpbrowser": "*@dev", + "codeception/module-webdriver": "*@dev", "codeception/util-universalframework": "*@dev", + "doctrine/orm": "^3.3", "ext-simplexml": "*", "jetbrains/phpstorm-attributes": "^1.0", - "symfony/dotenv": ">=4.4.24 <8.0", - "symfony/process": ">=4.4.24 <8.0", + "laravel-zero/phar-updater": "^1.4", + "php-webdriver/webdriver": "^1.15", + "stecman/symfony-console-completion": "^0.14", + "symfony/dotenv": ">=5.4.24 <8.0", + "symfony/error-handler": ">=5.4.24 <8.0", + "symfony/process": ">=5.4.24 <8.0", "vlucas/phpdotenv": "^5.1" }, "suggest": { @@ -7971,6 +8065,11 @@ "codecept" ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.2.x-dev" + } + }, "autoload": { "files": [ "functions.php" @@ -8005,7 +8104,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/5.1.2" + "source": "https://github.com/Codeception/Codeception/tree/5.3.2" }, "funding": [ { @@ -8013,20 +8112,20 @@ "type": "open_collective" } ], - "time": "2024-03-07T07:19:42+00:00" + "time": "2025-05-26T07:47:39+00:00" }, { "name": "codeception/lib-asserts", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "b8c7dff552249e560879c682ba44a4b963af91bc" + "reference": "06750a60af3ebc66faab4313981accec1be4eefc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/b8c7dff552249e560879c682ba44a4b963af91bc", - "reference": "b8c7dff552249e560879c682ba44a4b963af91bc", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/06750a60af3ebc66faab4313981accec1be4eefc", + "reference": "06750a60af3ebc66faab4313981accec1be4eefc", "shasum": "" }, "require": { @@ -8065,22 +8164,22 @@ ], "support": { "issues": "https://github.com/Codeception/lib-asserts/issues", - "source": "https://github.com/Codeception/lib-asserts/tree/2.1.0" + "source": "https://github.com/Codeception/lib-asserts/tree/2.2.0" }, - "time": "2023-02-10T18:36:23+00:00" + "time": "2025-03-10T20:41:33+00:00" }, { "name": "codeception/lib-innerbrowser", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "66b66d62b2b7f8595e257760b897190474cfb892" + "reference": "74476dd019ec7900b26b7dca91a42fdcb04e549f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/66b66d62b2b7f8595e257760b897190474cfb892", - "reference": "66b66d62b2b7f8595e257760b897190474cfb892", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/74476dd019ec7900b26b7dca91a42fdcb04e549f", + "reference": "74476dd019ec7900b26b7dca91a42fdcb04e549f", "shasum": "" }, "require": { @@ -8090,7 +8189,7 @@ "ext-json": "*", "ext-mbstring": "*", "php": "^8.1", - "phpunit/phpunit": "^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "symfony/browser-kit": "^4.4.24 || ^5.4 || ^6.0 || ^7.0", "symfony/dom-crawler": "^4.4.30 || ^5.4 || ^6.0 || ^7.0" }, @@ -8124,29 +8223,29 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/4.0.5" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/4.0.6" }, - "time": "2024-09-13T05:08:15+00:00" + "time": "2025-02-14T07:02:48+00:00" }, { "name": "codeception/lib-web", - "version": "1.0.6", + "version": "1.0.7", "source": { "type": "git", "url": "https://github.com/Codeception/lib-web.git", - "reference": "01ff7f9ed8760ba0b0805a0b3a843b4e74165a60" + "reference": "1444ccc9b1d6721f3ced8703c8f4a9041b80df93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-web/zipball/01ff7f9ed8760ba0b0805a0b3a843b4e74165a60", - "reference": "01ff7f9ed8760ba0b0805a0b3a843b4e74165a60", + "url": "https://api.github.com/repos/Codeception/lib-web/zipball/1444ccc9b1d6721f3ced8703c8f4a9041b80df93", + "reference": "1444ccc9b1d6721f3ced8703c8f4a9041b80df93", "shasum": "" }, "require": { "ext-mbstring": "*", "guzzlehttp/psr7": "^2.0", - "php": "^8.0", - "phpunit/phpunit": "^9.5 | ^10.0 | ^11.0", + "php": "^8.1", + "phpunit/phpunit": "^9.5 | ^10.0 | ^11.0 | ^12", "symfony/css-selector": ">=4.4.24 <8.0" }, "conflict": { @@ -8177,9 +8276,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-web/issues", - "source": "https://github.com/Codeception/lib-web/tree/1.0.6" + "source": "https://github.com/Codeception/lib-web/tree/1.0.7" }, - "time": "2024-02-06T20:50:08+00:00" + "time": "2025-02-09T12:05:55+00:00" }, { "name": "codeception/lib-xml", @@ -8232,22 +8331,22 @@ }, { "name": "codeception/module-asserts", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "1b6b150b30586c3614e7e5761b31834ed7968603" + "reference": "eb1f7c980423888f3def5116635754ae4a75bd47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/1b6b150b30586c3614e7e5761b31834ed7968603", - "reference": "1b6b150b30586c3614e7e5761b31834ed7968603", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/eb1f7c980423888f3def5116635754ae4a75bd47", + "reference": "eb1f7c980423888f3def5116635754ae4a75bd47", "shasum": "" }, "require": { "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^2.0", - "php": "^8.0" + "codeception/lib-asserts": "^2.2", + "php": "^8.2" }, "conflict": { "codeception/codeception": "<5.0" @@ -8283,9 +8382,9 @@ ], "support": { "issues": "https://github.com/Codeception/module-asserts/issues", - "source": "https://github.com/Codeception/module-asserts/tree/3.0.0" + "source": "https://github.com/Codeception/module-asserts/tree/3.2.0" }, - "time": "2022-02-16T19:48:08+00:00" + "time": "2025-05-02T02:33:11+00:00" }, { "name": "codeception/module-datafactory", @@ -8400,16 +8499,16 @@ }, { "name": "codeception/module-rest", - "version": "3.4.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-rest.git", - "reference": "086762ee8a1686e954678b015a7dca4b922c6520" + "reference": "5db3a2e62ce6948d1822709c034a22fa1b1f2309" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-rest/zipball/086762ee8a1686e954678b015a7dca4b922c6520", - "reference": "086762ee8a1686e954678b015a7dca4b922c6520", + "url": "https://api.github.com/repos/Codeception/module-rest/zipball/5db3a2e62ce6948d1822709c034a22fa1b1f2309", + "reference": "5db3a2e62ce6948d1822709c034a22fa1b1f2309", "shasum": "" }, "require": { @@ -8417,9 +8516,9 @@ "codeception/lib-xml": "^1.0", "ext-dom": "*", "ext-json": "*", - "justinrainbow/json-schema": "^5.2.9", + "justinrainbow/json-schema": "^5.2.9 || ^6", "php": "^8.1", - "softcreatr/jsonpath": "^0.8 || ^0.9" + "softcreatr/jsonpath": "^0.8 || ^0.9 || ^0.10" }, "require-dev": { "codeception/lib-innerbrowser": "^3.0 | ^4.0", @@ -8454,9 +8553,9 @@ ], "support": { "issues": "https://github.com/Codeception/module-rest/issues", - "source": "https://github.com/Codeception/module-rest/tree/3.4.0" + "source": "https://github.com/Codeception/module-rest/tree/3.4.1" }, - "time": "2024-07-12T06:28:28+00:00" + "time": "2025-03-25T23:04:38+00:00" }, { "name": "codeception/module-yii2", @@ -8521,21 +8620,21 @@ }, { "name": "codeception/stub", - "version": "4.1.3", + "version": "4.1.4", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "4fcad2c165f365377486dc3fd8703b07f1f2fcae" + "reference": "6ce453073a0c220b254dd7f4383645615e4071c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/4fcad2c165f365377486dc3fd8703b07f1f2fcae", - "reference": "4fcad2c165f365377486dc3fd8703b07f1f2fcae", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/6ce453073a0c220b254dd7f4383645615e4071c3", + "reference": "6ce453073a0c220b254dd7f4383645615e4071c3", "shasum": "" }, "require": { "php": "^7.4 | ^8.0", - "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11" + "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11 | ^12" }, "conflict": { "codeception/codeception": "<5.0.6" @@ -8556,27 +8655,104 @@ "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", "support": { "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/4.1.3" + "source": "https://github.com/Codeception/Stub/tree/4.1.4" }, - "time": "2024-02-02T19:21:00+00:00" + "time": "2025-02-14T06:56:33+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.7", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.7" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-05-26T15:08:54+00:00" }, { "name": "craftcms/ckeditor", - "version": "4.5.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/craftcms/ckeditor.git", - "reference": "7a366bae76e17c96bc3eae18d0db9764799dd7ec" + "reference": "df2ea2ec6a689b276507c83c38eb982a0287651a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/ckeditor/zipball/7a366bae76e17c96bc3eae18d0db9764799dd7ec", - "reference": "7a366bae76e17c96bc3eae18d0db9764799dd7ec", + "url": "https://api.github.com/repos/craftcms/ckeditor/zipball/df2ea2ec6a689b276507c83c38eb982a0287651a", + "reference": "df2ea2ec6a689b276507c83c38eb982a0287651a", "shasum": "" }, "require": { "craftcms/cms": "^5.6.0", - "craftcms/html-field": "^3.1.0", + "craftcms/html-field": "^3.4.0", + "embed/embed": "^4.4", "nystudio107/craft-code-editor": ">=1.0.8 <=1.0.13 || ^1.0.16", "php": "^8.2" }, @@ -8622,7 +8798,7 @@ "rss": "https://github.com/craftcms/ckeditor/commits/master.atom", "source": "https://github.com/craftcms/ckeditor" }, - "time": "2025-01-23T20:00:00+00:00" + "time": "2025-05-23T15:58:04+00:00" }, { "name": "craftcms/ecs", @@ -8659,21 +8835,24 @@ }, { "name": "craftcms/html-field", - "version": "3.1.1", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/craftcms/html-field.git", - "reference": "50a56e4cb4d4511b39674965e0fbff475f62f7f0" + "reference": "3f23569c94d64e9054e3402447e03bd3c4c7b181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/html-field/zipball/50a56e4cb4d4511b39674965e0fbff475f62f7f0", - "reference": "50a56e4cb4d4511b39674965e0fbff475f62f7f0", + "url": "https://api.github.com/repos/craftcms/html-field/zipball/3f23569c94d64e9054e3402447e03bd3c4c7b181", + "reference": "3f23569c94d64e9054e3402447e03bd3c4c7b181", "shasum": "" }, "require": { - "craftcms/cms": "^5.0.0-beta.1", - "php": "^8.2" + "craftcms/cms": "^5.5.0", + "league/html-to-markdown": "^5.1", + "php": "^8.2", + "symfony/css-selector": "^6.0|^7.0", + "symfony/dom-crawler": "^6.0|^7.0" }, "require-dev": { "craftcms/ecs": "dev-main", @@ -8704,7 +8883,7 @@ "rss": "https://github.com/craftcms/html-field/commits/main.atom", "source": "https://github.com/craftcms/html-field" }, - "time": "2024-10-15T20:02:05+00:00" + "time": "2025-04-30T16:54:07+00:00" }, { "name": "craftcms/phpstan", @@ -8835,6 +9014,95 @@ "abandoned": "craftcms/ckeditor", "time": "2024-09-03T13:38:27+00:00" }, + { + "name": "embed/embed", + "version": "v4.4.17", + "source": { + "type": "git", + "url": "https://github.com/php-embed/Embed.git", + "reference": "b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-embed/Embed/zipball/b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87", + "reference": "b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "ext-curl": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ml/json-ld": "^1.1", + "oscarotero/html-parser": "^0.1.4", + "php": "^7.4|^8", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0|^2.0" + }, + "require-dev": { + "brick/varexporter": "^0.3.1", + "friendsofphp/php-cs-fixer": "^2.0", + "nyholm/psr7": "^1.2", + "oscarotero/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^9.0", + "symfony/css-selector": "^5.0" + }, + "suggest": { + "symfony/css-selector": "If you want to get elements using css selectors" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Embed\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP library to retrieve page info using oembed, opengraph, etc", + "homepage": "https://github.com/oscarotero/Embed", + "keywords": [ + "embed", + "embedly", + "oembed", + "opengraph", + "twitter cards" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/Embed/issues", + "source": "https://github.com/php-embed/Embed/tree/v4.4.17" + }, + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2025-05-13T12:42:29+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", @@ -8962,30 +9230,40 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.4.1", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/35d262c94959571e8736db1e5c9bc36ab94ae900", + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -9014,16 +9292,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.1" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-04-04T13:08:07+00:00" }, { "name": "league/factory-muffin", @@ -9168,18 +9446,284 @@ ], "time": "2020-12-13T15:53:28+00:00" }, + { + "name": "league/html-to-markdown", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.1.0", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^8.5 || ^9.2", + "scrutinizer/ocular": "^1.6", + "unleashedtech/php-coding-standard": "^2.7 || ^3.0", + "vimeo/psalm": "^4.22 || ^5.0" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "support": { + "issues": "https://github.com/thephpleague/html-to-markdown/issues", + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" + } + ], + "time": "2023-07-12T21:21:09+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, + { + "name": "ml/iri", + "version": "1.1.4", + "target-dir": "ML/IRI", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/IRI.git", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341", + "shasum": "" + }, + "require": { + "lib-pcre": ">=4.0", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "ML\\IRI": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "IRI handling for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "URN", + "iri", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/lanthaler/IRI/issues", + "source": "https://github.com/lanthaler/IRI/tree/master" + }, + "time": "2014-01-21T13:43:39+00:00" + }, + { + "name": "ml/json-ld", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/JsonLD.git", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ml/iri": "^1.1.1", + "php": ">=5.3.0" + }, + "require-dev": { + "json-ld/tests": "1.0", + "phpunit/phpunit": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ML\\JsonLD\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "JSON-LD Processor for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "JSON-LD", + "jsonld" + ], + "support": { + "issues": "https://github.com/lanthaler/JsonLD/issues", + "source": "https://github.com/lanthaler/JsonLD/tree/1.2.1" + }, + "time": "2022-09-29T08:45:17+00:00" + }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -9218,7 +9762,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -9226,7 +9770,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nikic/php-parser", @@ -9288,16 +9832,16 @@ }, { "name": "nystudio107/craft-code-editor", - "version": "1.0.22", + "version": "1.0.23", "source": { "type": "git", "url": "https://github.com/nystudio107/craft-code-editor.git", - "reference": "170edf71355b659e1db9ede12980b17c20eb3d1f" + "reference": "0a206d73efa8f8d3bd7294ae68b3d70c696b19c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nystudio107/craft-code-editor/zipball/170edf71355b659e1db9ede12980b17c20eb3d1f", - "reference": "170edf71355b659e1db9ede12980b17c20eb3d1f", + "url": "https://api.github.com/repos/nystudio107/craft-code-editor/zipball/0a206d73efa8f8d3bd7294ae68b3d70c696b19c5", + "reference": "0a206d73efa8f8d3bd7294ae68b3d70c696b19c5", "shasum": "" }, "require": { @@ -9352,7 +9896,60 @@ "type": "github" } ], - "time": "2024-09-23T17:20:25+00:00" + "time": "2025-05-17T20:38:20+00:00" + }, + { + "name": "oscarotero/html-parser", + "version": "v0.1.8", + "source": { + "type": "git", + "url": "https://github.com/oscarotero/html-parser.git", + "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oscarotero/html-parser/zipball/10f3219267a365d9433f2f7d1694209c9d436c8d", + "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.11", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HtmlParser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "Parse html strings to DOMDocument", + "homepage": "https://github.com/oscarotero/html-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/html-parser/issues", + "source": "https://github.com/oscarotero/html-parser/tree/v0.1.8" + }, + "time": "2023-11-29T20:28:41+00:00" }, { "name": "phar-io/manifest", @@ -9549,16 +10146,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.16", + "version": "1.12.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e0bb5cb78545aae631220735aa706eac633a6be9", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", "shasum": "" }, "require": { @@ -9603,27 +10200,27 @@ "type": "github" } ], - "time": "2025-01-21T14:50:05+00:00" + "time": "2025-05-21T20:51:45+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.8", + "version": "11.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", + "nikic/php-parser": "^5.4.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", @@ -9635,7 +10232,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.5.0" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9673,7 +10270,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/11.0.8" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" }, "funding": [ { @@ -9681,7 +10278,7 @@ "type": "github" } ], - "time": "2024-12-11T12:34:27+00:00" + "time": "2025-02-25T13:26:39+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9930,16 +10527,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.5", + "version": "11.5.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b9a975972f580c0491f834eb0818ad2b32fd8bba" + "reference": "d565e2cdc21a7db9dc6c399c1fc2083b8010f289" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b9a975972f580c0491f834eb0818ad2b32fd8bba", - "reference": "b9a975972f580c0491f834eb0818ad2b32fd8bba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d565e2cdc21a7db9dc6c399c1fc2083b8010f289", + "reference": "d565e2cdc21a7db9dc6c399c1fc2083b8010f289", "shasum": "" }, "require": { @@ -9949,24 +10546,24 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.8", + "phpunit/php-code-coverage": "^11.0.9", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", - "sebastian/comparator": "^6.3.0", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.0", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", + "sebastian/type": "^5.1.2", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" }, @@ -10011,7 +10608,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.5" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.21" }, "funding": [ { @@ -10022,25 +10619,33 @@ "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/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-01-29T14:01:11+00:00" + "time": "2025-05-21T12:35:00+00:00" }, { "name": "psy/psysh", - "version": "v0.12.7", + "version": "v0.12.8", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", "shasum": "" }, "require": { @@ -10104,9 +10709,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" }, - "time": "2024-12-10T01:58:33+00:00" + "time": "2025-03-16T03:05:19+00:00" }, { "name": "rector/rector", @@ -10226,16 +10831,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -10271,7 +10876,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -10279,7 +10884,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -10339,16 +10944,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.0", + "version": "6.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "shasum": "" }, "require": { @@ -10367,7 +10972,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.2-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -10407,7 +11012,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" }, "funding": [ { @@ -10415,7 +11020,7 @@ "type": "github" } ], - "time": "2025-01-06T10:28:19+00:00" + "time": "2025-03-07T06:57:01+00:00" }, { "name": "sebastian/complexity", @@ -10544,23 +11149,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -10596,15 +11201,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "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/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", @@ -10984,16 +11601,16 @@ }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "shasum": "" }, "require": { @@ -11029,7 +11646,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" }, "funding": [ { @@ -11037,7 +11654,7 @@ "type": "github" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-03-18T13:35:50+00:00" }, { "name": "sebastian/version", @@ -11095,16 +11712,16 @@ }, { "name": "softcreatr/jsonpath", - "version": "0.9.1", + "version": "0.10.0", "source": { "type": "git", "url": "https://github.com/SoftCreatR/JSONPath.git", - "reference": "272173a65fd16b25010dbd54d04dd34c0c5a8500" + "reference": "74f0b330a98135160db947ba7bc65216b64a0c86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/272173a65fd16b25010dbd54d04dd34c0c5a8500", - "reference": "272173a65fd16b25010dbd54d04dd34c0c5a8500", + "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/74f0b330a98135160db947ba7bc65216b64a0c86", + "reference": "74f0b330a98135160db947ba7bc65216b64a0c86", "shasum": "" }, "require": { @@ -11160,7 +11777,7 @@ "type": "github" } ], - "time": "2024-06-01T09:15:21+00:00" + "time": "2025-03-22T00:28:17+00:00" }, { "name": "staabm/side-effects-detector", @@ -11216,16 +11833,16 @@ }, { "name": "symfony/browser-kit", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94" + "reference": "5384291845e74fd7d54f3d925c4a86ce12336593" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8d64d17e198082f8f198d023a6b634e7b5fdda94", - "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/5384291845e74fd7d54f3d925c4a86ce12336593", + "reference": "5384291845e74fd7d54f3d925c4a86ce12336593", "shasum": "" }, "require": { @@ -11264,7 +11881,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.2.0" + "source": "https://github.com/symfony/browser-kit/tree/v7.3.0" }, "funding": [ { @@ -11280,27 +11897,28 @@ "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-03-05T10:15:41+00:00" }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -11357,7 +11975,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.3.0" }, "funding": [ { @@ -11373,20 +11991,20 @@ "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2025-05-24T10:34:04+00:00" }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", "shasum": "" }, "require": { @@ -11421,7 +12039,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.0" }, "funding": [ { @@ -11437,20 +12055,20 @@ "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2024-12-30T19:00:26+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -11501,7 +12119,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -11517,7 +12135,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symplify/easy-coding-standard", @@ -11626,16 +12244,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -11694,7 +12312,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -11706,7 +12324,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" } ], "aliases": [], diff --git a/rector.php b/rector.php index b91f8e4efa..83d0991d94 100644 --- a/rector.php +++ b/rector.php @@ -9,4 +9,10 @@ __DIR__ . '/src', __DIR__ . '/tests/unit', ]) - ->withPhpSets(php73: true); + ->withSkip([ + Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class => [ + __DIR__ . '/src/console/controllers/GatewaysController.php', + ], + Rector\Php80\Rector\Class_\StringableForToStringRector::class, + ]) + ->withPhpSets(php80: true); diff --git a/src/Plugin.php b/src/Plugin.php index dab0755fe2..f64c6d4888 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -257,7 +257,7 @@ public static function editions(): array /** * @inheritDoc */ - public string $schemaVersion = '5.3.2.2'; + public string $schemaVersion = '5.4.0.3'; /** * @inheritdoc @@ -1156,7 +1156,7 @@ private function _defineFieldLayoutElements(): void } /** - * Defines the `resave/products` command. + * Defines the `resave/products`, `resave/variants`, `resave/carts` and `resave/orders` commands. */ private function _defineResaveCommand(): void { @@ -1171,10 +1171,9 @@ private function _defineResaveCommand(): void $criteria['type'] = explode(',', $controller->type); } - // @TODO Remove this check when Commerce requires Craft 5.5 - if (version_compare(Craft::$app->getInfo()->version, '5.5.0', '>=') && !empty($controller->withFields)) { + if (!empty($controller->withFields)) { $handles = Collection::make(self::getInstance()->getProductTypes()->getAllProductTypes()) - ->filter(fn(ProductType $productType) => $controller->hasTheFields($productType->getFieldLayout())) + ->filter(fn(ProductType $productType) => $controller->hasTheFields($productType->getProductFieldLayout())) ->map(fn(ProductType $productType) => $productType->handle) ->all(); if (isset($criteria['type'])) { @@ -1198,6 +1197,41 @@ private function _defineResaveCommand(): void ], ]; + $e->actions['variants'] = [ + 'action' => function(): int { + /** @var ResaveController $controller */ + $controller = Craft::$app->controller; + $criteria = []; + + if ($controller->type !== null) { + $criteria['type'] = explode(',', $controller->type); + } + if (!empty($controller->withFields)) { + $handles = Collection::make(self::getInstance()->getProductTypes()->getAllProductTypes()) + ->filter(fn(ProductType $productType) => $controller->hasTheFields($productType->getVariantFieldLayout())) + ->map(fn(ProductType $productType) => $productType->handle) + ->all(); + if (isset($criteria['type'])) { + $criteria['type'] = array_intersect($criteria['type'], $handles); + } else { + $criteria['type'] = $handles; + } + + if (empty($criteria['type'])) { + $controller->output($controller->markdownToAnsi('No variant types satisfy `--with-fields`.')); + return ExitCode::UNSPECIFIED_ERROR; + } + } + + return $controller->resaveElements(Variant::class, $criteria); + }, + 'options' => array_filter(['type', (property_exists(ResaveController::class, 'withFields') ? 'withFields' : null)]), + 'helpSummary' => 'Re-saves Commerce variants.', + 'optionsHelp' => [ + 'type' => 'The product type handle(s) of the variants to resave.', + ], + ]; + $e->actions['orders'] = [ 'action' => function(): int { /** @var ResaveController $controller */ diff --git a/src/adjusters/Discount.php b/src/adjusters/Discount.php index 2966a7e585..420057ddf3 100644 --- a/src/adjusters/Discount.php +++ b/src/adjusters/Discount.php @@ -173,9 +173,7 @@ public function adjust(Order $order): array }, SORT_DESC); // Remove non-promotable line items - $lineItemsByPrice = ArrayHelper::where($lineItemsByPrice, function(LineItem $lineItem) { - return $lineItem->getIsPromotable(); - }, true, true); + $lineItemsByPrice = ArrayHelper::where($lineItemsByPrice, fn(LineItem $lineItem) => $lineItem->getIsPromotable(), true, true); // Loop over each order level adjustment and add an adjustment to each line item until it runs out. foreach ($orderLevelAdjustments as $orderLevelAdjustment) { diff --git a/src/base/Gateway.php b/src/base/Gateway.php index e6af0b0c9d..424fac8e03 100644 --- a/src/base/Gateway.php +++ b/src/base/Gateway.php @@ -9,9 +9,13 @@ use Craft; use craft\base\SavableComponent; +use craft\commerce\elements\conditions\orders\DiscountOrderCondition; +use craft\commerce\elements\conditions\orders\GatewayOrderCondition; use craft\commerce\elements\Order; use craft\commerce\models\payments\BasePaymentForm; use craft\commerce\models\Transaction; +use craft\elements\conditions\ElementConditionInterface; +use craft\helpers\Json; use craft\helpers\StringHelper; use craft\helpers\UrlHelper; @@ -32,6 +36,12 @@ abstract class Gateway extends SavableComponent implements GatewayInterface { use GatewayTrait; + /** + * @var ElementConditionInterface|null + * @since 5.4.0 + */ + private ?ElementConditionInterface $_orderCondition = null; + /** * Returns the name of this payment method. * @@ -103,7 +113,7 @@ public function defineRules(): array $rules = parent::defineRules(); $rules[] = [['paymentType', 'handle'], 'required']; - $rules[] = [['name', 'handle', 'paymentType', 'isFrontendEnabled', 'sortOrder'], 'safe']; + $rules[] = [['name', 'handle', 'paymentType', 'isFrontendEnabled', 'orderCondition', 'sortOrder'], 'safe']; return $rules; } @@ -124,6 +134,10 @@ public function getPaymentConfirmationFormHtml(array $params): string */ public function availableForUseWithOrder(Order $order): bool { + if ($this->hasOrderCondition() && !$this->getOrderCondition()->matchElement($order)) { + return false; + } + return true; } @@ -135,6 +149,16 @@ public function supportsPartialPayment(): bool return true; } + /** + * Returns true if this gateway has an order condition + * + * @since 5.4.0 + */ + public function hasOrderCondition(): bool + { + return $this->getOrderCondition()->getConditionRules() !== []; + } + /** * Returns payment Form HTML */ @@ -157,4 +181,66 @@ public function transactionSupportsRefund(Transaction $transaction): bool { return true; } + + + /** + * Gets the order condition for this gateway + * + * @since 5.4.0 + */ + public function getOrderCondition(): ElementConditionInterface + { + /** @var DiscountOrderCondition $condition */ + $condition = $this->_orderCondition ?? new GatewayOrderCondition(); + $condition->mainTag = 'div'; + $condition->name = 'orderCondition'; + + return $condition; + } + + /** + * Sets the order condition for this gateway + * + * @since 5.4.0 + */ + public function setOrderCondition(ElementConditionInterface|string|array $condition): void + { + if (empty($condition)) { + $this->_orderCondition = null; + return; + } + + if (is_string($condition)) { + $condition = Json::decodeIfJson($condition); + } + + if (!$condition instanceof GatewayOrderCondition) { + $condition['class'] = GatewayOrderCondition::class; + /** @var GatewayOrderCondition $condition */ + $condition = \Craft::$app->getConditions()->createCondition($condition); + } + $condition->forProjectConfig = true; + + $this->_orderCondition = $condition; + } + + /** + * @return array + * @since 5.4.0 + */ + public function getConfig(): array + { + $configData = [ + 'name' => $this->name, + 'handle' => $this->handle, + 'type' => static::class, + 'settings' => $this->getSettings(), + 'sortOrder' => ($this->sortOrder ?? 99), + 'paymentType' => $this->paymentType, + 'isFrontendEnabled' => $this->getIsFrontendEnabled(false), + 'orderCondition' => $this->getOrderCondition()->getConfig(), + ]; + + return $configData; + } } diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 053e9a4b9d..63a04bbbb4 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -17,6 +17,7 @@ use craft\commerce\helpers\Currency; use craft\commerce\helpers\Localization; use craft\commerce\helpers\Purchasable as PurchasableHelper; +use craft\commerce\models\CatalogPricingRule; use craft\commerce\models\InventoryItem; use craft\commerce\models\InventoryLevel; use craft\commerce\models\LineItem; @@ -184,6 +185,20 @@ abstract class Purchasable extends Element implements PurchasableInterface, HasS */ private ?float $_basePromotionalPrice = null; + /** + * The ID of the catalog pricing rule that is affecting the sale price of this purchasable. + * + * @var int|null + * @since 5.4.0 + */ + public ?int $catalogPricingRuleId = null; + + /** + * @var CatalogPricingRule|null + * @since 5.4.0 + * @see getCatalogPricingRule() + */ + private ?CatalogPricingRule $_catalogPricingRule = null; /** * @var bool @@ -616,6 +631,21 @@ public function setPromotionalPrice(?float $price): void $this->_promotionalPrice = $price; } + /** + * @return CatalogPricingRule|null + * @throws InvalidConfigException + * @throws SiteNotFoundException + * @since 5.4.0 + */ + public function getCatalogPricingRule(): ?CatalogPricingRule + { + if ($this->_catalogPricingRule === null && $this->catalogPricingRuleId !== null) { + $this->_catalogPricingRule = Plugin::getInstance()->getCatalogPricingRules()->getCatalogPricingRuleById($this->catalogPricingRuleId, $this->storeId); + } + + return $this->_catalogPricingRule; + } + /** * @inheritdoc */ @@ -707,7 +737,9 @@ public function getTaxCategory(): TaxCategory */ public function getSnapshot(): array { - return []; + return [ + 'catalogPricingRuleId' => $this->catalogPricingRuleId, + ]; } /** diff --git a/src/base/ShippingMethod.php b/src/base/ShippingMethod.php index 42224763b5..01733de0d4 100644 --- a/src/base/ShippingMethod.php +++ b/src/base/ShippingMethod.php @@ -9,6 +9,7 @@ use Craft; use craft\commerce\base\Model as BaseModel; +use craft\commerce\elements\conditions\customers\ShippingMethodCustomerCondition; use craft\commerce\elements\conditions\orders\ShippingMethodOrderCondition; use craft\commerce\elements\Order; use craft\commerce\errors\NotImplementedException; @@ -27,6 +28,8 @@ * @property-read array $shippingRules * @property-read bool $isEnabled * @property-read string $type + * @property ShippingMethodOrderCondition $orderCondition + * @property ShippingMethodCustomerCondition $customerCondition */ abstract class ShippingMethod extends BaseModel implements ShippingMethodInterface, HasStoreInterface { @@ -54,10 +57,20 @@ abstract class ShippingMethod extends BaseModel implements ShippingMethodInterfa /** * @var ShippingMethodOrderCondition|null + * @see getOrderCondition() + * @see setOrderCondition() * @since 5.0.0 */ private ?ShippingMethodOrderCondition $_orderCondition = null; + /** + * @var ShippingMethodCustomerCondition|null + * @see getCustomerCondition() + * @see setCustomerCondition() + * @since 5.4.0 + */ + private ?ShippingMethodCustomerCondition $_customerCondition = null; + /** * @var DateTime|null * @since 3.4 @@ -138,6 +151,7 @@ protected function defineRules(): array 'handle', 'storeId', 'orderCondition', + 'customerCondition', 'enabled', 'dateCreated', 'dateUpdated', @@ -182,6 +196,41 @@ public function getOrderCondition(): ShippingMethodOrderCondition return $condition; } + /** + * @param ShippingMethodCustomerCondition|string|array|null $condition + * @return void + * @throws InvalidConfigException + * @since 5.4.0 + */ + public function setCustomerCondition(ShippingMethodCustomerCondition|string|array|null $condition): void + { + if (is_string($condition)) { + $condition = Json::decodeIfJson($condition); + } + + if (!$condition instanceof ShippingMethodCustomerCondition) { + $condition['class'] = ShippingMethodCustomerCondition::class; + $condition = Craft::$app->getConditions()->createCondition($condition); + /** @var ShippingMethodCustomerCondition $condition */ + } + $condition->forProjectConfig = false; + + $this->_customerCondition = $condition; + } + + /** + * @return ShippingMethodCustomerCondition + * @since 5.0.0 + */ + public function getCustomerCondition(): ShippingMethodCustomerCondition + { + $condition = $this->_customerCondition ?? new ShippingMethodCustomerCondition(); + $condition->mainTag = 'div'; + $condition->name = 'customerCondition'; + + return $condition; + } + /** * @inheritdoc */ @@ -192,6 +241,17 @@ public function matchOrder(Order $order): bool return false; } + $customer = $order->getCustomer(); + // If there is no customer on the order and there are customer conditions, we can't match. + if (!$customer && !empty($this->getCustomerCondition()->getConditionRules())) { + return false; + } + + // Match the method's customer condition. + if ($customer && !$this->getCustomerCondition()->matchElement($customer)) { + return false; + } + /** @var ShippingRuleInterface $rule */ foreach ($this->getShippingRules()->all() as $rule) { if ($rule->matchOrder($order)) { diff --git a/src/base/Stat.php b/src/base/Stat.php index 5313f87742..6bc71ea77a 100644 --- a/src/base/Stat.php +++ b/src/base/Stat.php @@ -431,9 +431,7 @@ public function getOrderStatuses(): ?array continue; } - $orderStatus = ArrayHelper::firstWhere($allOrderStatuses, function(OrderStatus $os) use ($orderStatus) { - return $orderStatus === $os->handle || $orderStatus === $os->uid; - }); + $orderStatus = ArrayHelper::firstWhere($allOrderStatuses, fn(OrderStatus $os) => $orderStatus === $os->handle || $orderStatus === $os->uid); if (!$orderStatus) { unset($this->_orderStatuses[$key]); continue; diff --git a/src/base/Zone.php b/src/base/Zone.php index e5546ab70b..62ac98c3c1 100644 --- a/src/base/Zone.php +++ b/src/base/Zone.php @@ -49,7 +49,7 @@ abstract class Zone extends BaseModel implements ZoneInterface, HasStoreInterfac /** * @var ?ZoneAddressCondition */ - private ?ZoneAddressCondition $_condition; + private ?ZoneAddressCondition $_condition = null; abstract public function getCpEditUrl(): string; diff --git a/src/behaviors/CurrencyAttributeBehavior.php b/src/behaviors/CurrencyAttributeBehavior.php index d16f0be234..9dcbbe77e1 100644 --- a/src/behaviors/CurrencyAttributeBehavior.php +++ b/src/behaviors/CurrencyAttributeBehavior.php @@ -68,7 +68,7 @@ class CurrencyAttributeBehavior extends Behavior * @uses setDefaultCurrency() * @uses getDefaultCurrency() */ - private ?string $_defaultCurrency; + private ?string $_defaultCurrency = null; /** * @var array mapping of attribute => currency if the default is not desired diff --git a/src/collections/InventoryMovementCollection.php b/src/collections/InventoryMovementCollection.php index 527406febe..7b3053b9b1 100644 --- a/src/collections/InventoryMovementCollection.php +++ b/src/collections/InventoryMovementCollection.php @@ -29,8 +29,6 @@ class InventoryMovementCollection extends Collection */ public function getPurchasables(): array { - return $this->map(function(InventoryMovementInterface $updateInventoryLevel) { - return $updateInventoryLevel->getInventoryItem()->getPurchasable(); - })->all(); + return $this->map(fn(InventoryMovementInterface $updateInventoryLevel) => $updateInventoryLevel->getInventoryItem()->getPurchasable())->all(); } } diff --git a/src/collections/UpdateInventoryLevelCollection.php b/src/collections/UpdateInventoryLevelCollection.php index a78ba9600f..f689316803 100644 --- a/src/collections/UpdateInventoryLevelCollection.php +++ b/src/collections/UpdateInventoryLevelCollection.php @@ -47,8 +47,6 @@ public static function make($items = []) */ public function getPurchasables(): array { - return $this->map(function(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) { - return $updateInventoryLevel->getInventoryItem()->getPurchasable(); - })->filter()->all(); + return $this->map(fn(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) => $updateInventoryLevel->getInventoryItem()->getPurchasable())->filter()->all(); } } diff --git a/src/controllers/CartController.php b/src/controllers/CartController.php index 8fb03a489d..43aaf18298 100644 --- a/src/controllers/CartController.php +++ b/src/controllers/CartController.php @@ -164,12 +164,15 @@ public function actionUpdateCart(): ?Response $options = $this->request->getParam('options', []); // TODO Commerce 4 should only support key value only #COM-55 $qty = (int)$this->request->getParam('qty', 1); + $params = compact('qty', 'note', 'purchasableId', 'options'); + if ($qty > 0) { // We only want a new line item if they cleared the cart if ($clearLineItems) { - $lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, compact('purchasableId', 'options')); + $lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, params: $params); } else { - $lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $purchasableId, $options); + // we are passing everything into params but need to pass purchasableId and options for now until we refactor + $lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $params['purchasableId'], $params['options'], params: $params); } // New line items already have a qty of one. @@ -216,15 +219,18 @@ public function actionUpdateCart(): ?Response // Ignore zero value qty for multi-add forms https://github.com/craftcms/commerce/issues/330#issuecomment-384533139 if ($purchasable['qty'] > 0) { + $params = [ + 'purchasableId' => $purchasable['id'], + 'options' => $purchasable['options'], + 'note' => $purchasable['note'], + 'qty' => $purchasable['qty'], + ]; // We only want a new line item if they cleared the cart if ($clearLineItems) { - $lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, [ - 'purchasableId' => $purchasable['id'], - 'options' => $purchasable['options'], - ]); + $lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, params: $params); } else { - $lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $purchasable['id'], $purchasable['options']); + $lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $params['purchasableId'], $params['options'], $params); } // New line items already have a qty of one. diff --git a/src/controllers/CatalogPricingRulesController.php b/src/controllers/CatalogPricingRulesController.php index c5838a76c7..1d5fe92442 100755 --- a/src/controllers/CatalogPricingRulesController.php +++ b/src/controllers/CatalogPricingRulesController.php @@ -86,7 +86,7 @@ public function actionEdit(?string $storeHandle = null, int $id = null, CatalogP $store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle); } - $store = $store ?? Plugin::getInstance()->getStores()->getPrimaryStore(); + $store ??= Plugin::getInstance()->getStores()->getPrimaryStore(); $variables = compact('id', 'catalogPricingRule', 'storeHandle'); @@ -326,7 +326,7 @@ public function actionUpdateStatus(): void /** @var CatalogPricingRuleRecord $rule */ foreach ($rules as $rule) { - $storeId = $storeId ?? $rule->storeId; + $storeId ??= $rule->storeId; $rule->enabled = ($status == 'enabled'); $rule->save(); } diff --git a/src/controllers/DiscountsController.php b/src/controllers/DiscountsController.php index c7e4b2af72..1cb4beda4f 100644 --- a/src/controllers/DiscountsController.php +++ b/src/controllers/DiscountsController.php @@ -39,7 +39,6 @@ use yii\web\HttpException; use yii\web\Response; use function explode; -use function get_class; /** * Class Discounts Controller @@ -506,17 +505,11 @@ public function actionClearDiscountUses(): Response return $this->asFailure(Craft::t('commerce', 'Type not in allowed options.')); } - switch ($type) { - case self::DISCOUNT_COUNTER_TYPE_EMAIL: - Plugin::getInstance()->getDiscounts()->clearEmailUsageHistoryById($id); - break; - case self::DISCOUNT_COUNTER_TYPE_CUSTOMER: - Plugin::getInstance()->getDiscounts()->clearCustomerUsageHistoryById($id); - break; - case self::DISCOUNT_COUNTER_TYPE_TOTAL: - Plugin::getInstance()->getDiscounts()->clearDiscountUsesById($id); - break; - } + match ($type) { + self::DISCOUNT_COUNTER_TYPE_EMAIL => Plugin::getInstance()->getDiscounts()->clearEmailUsageHistoryById($id), + self::DISCOUNT_COUNTER_TYPE_CUSTOMER => Plugin::getInstance()->getDiscounts()->clearCustomerUsageHistoryById($id), + self::DISCOUNT_COUNTER_TYPE_TOTAL => Plugin::getInstance()->getDiscounts()->clearDiscountUsesById($id), + }; return $this->asSuccess(); } @@ -693,8 +686,8 @@ private function _populateVariables(array &$variables): void foreach ($purchasableIds as $purchasableId) { $purchasable = Craft::$app->getElements()->getElementById((int)$purchasableId); if ($purchasable instanceof PurchasableInterface) { - $class = get_class($purchasable); - $purchasables[$class] = $purchasables[$class] ?? []; + $class = $purchasable::class; + $purchasables[$class] ??= []; $purchasables[$class][] = $purchasable; } } diff --git a/src/controllers/GatewaysController.php b/src/controllers/GatewaysController.php index 7eddd0f986..dfc34b1458 100644 --- a/src/controllers/GatewaysController.php +++ b/src/controllers/GatewaysController.php @@ -103,15 +103,15 @@ public function actionEdit(?string $storeHandle = null, int $id = null, ?Gateway $allGatewayTypes = $gatewayService->getAllGatewayTypes(); // Make sure the selected gateway class is in there - if ($gateway && !in_array(get_class($gateway), $allGatewayTypes, true)) { - $allGatewayTypes[] = get_class($gateway); + if ($gateway && !in_array($gateway::class, $allGatewayTypes, true)) { + $allGatewayTypes[] = $gateway::class; } $gatewayInstances = []; $gatewayOptions = []; foreach ($allGatewayTypes as $class) { - if (($gateway && $class === get_class($gateway)) || $class::isSelectable()) { + if (($gateway && $class === $gateway::class) || $class::isSelectable()) { $gatewayInstances[$class] = $gatewayService->createGateway($class); $gatewayOptions[] = [ @@ -160,6 +160,12 @@ public function actionSave(): ?Response 'isFrontendEnabled' => $this->request->getParam('isFrontendEnabled'), 'settings' => $this->request->getBodyParam('types.' . $type), ]; + + // Handle order condition if it's in the request + $orderCondition = $this->request->getBodyParam('orderCondition'); + if ($orderCondition !== null) { + $config['orderCondition'] = $orderCondition; + } // For new gateway avoid NULL value. if (!$this->request->getBodyParam('id')) { diff --git a/src/controllers/InventoryController.php b/src/controllers/InventoryController.php index f38bf56497..55672982ab 100644 --- a/src/controllers/InventoryController.php +++ b/src/controllers/InventoryController.php @@ -575,9 +575,7 @@ public function actionEditUpdateLevelsModal(): Response 'inventoryItemIds' => $inventoryItemIds, 'inventoryLevels' => $inventoryLevels, 'updateAction' => $updateAction, - 'inventoryLocationOptions' => Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations()->mapWithKeys(function($location) { - return [$location->id => $location->getUiLabel()]; - })->all(), + 'inventoryLocationOptions' => Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations()->mapWithKeys(fn($location) => [$location->id => $location->getUiLabel()])->all(), 'type' => $type, 'quantity' => $quantity, 'note' => $note, diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 52f54eb8d5..3c6f855e6b 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -81,6 +81,7 @@ use yii\web\BadRequestHttpException; use yii\web\ForbiddenHttpException; use yii\web\HttpException; +use yii\web\MethodNotAllowedHttpException; use yii\web\Response; /** @@ -785,9 +786,7 @@ public function actionCustomerSearch(): Response $userQuery->search(urldecode($query)); } - $customers = $userQuery->collect()->map(function(User $user) { - return $this->_customerToArray($user); - }); + $customers = $userQuery->collect()->map(fn(User $user) => $this->_customerToArray($user)); return $this->asSuccess(data: compact('customers')); } @@ -820,11 +819,9 @@ public function actionGetCustomerAddresses(): Response $total = $addressElements->count(); - $addresses = $addressElements->map(function(Address $address) { - return $address->toArray() + [ - 'html' => Cp::elementCardHtml($address), - ]; - }); + $addresses = $addressElements->map(fn(Address $address) => $address->toArray() + [ + 'html' => Cp::elementCardHtml($address), + ]); return $this->asSuccess(data: compact('addresses', 'total')); } @@ -1011,6 +1008,49 @@ public function actionUpdateOrderAddress(): Response return $this->asSuccess(); } + /** + * @return Response + * @throws BadRequestHttpException + * @throws MethodNotAllowedHttpException + * @throws Throwable + * @since 5.4.0 + */ + public function actionCopyAddressToUser(): Response + { + $this->requirePermission('editUsers'); + $this->requirePostRequest(); + $this->requireAcceptsJson(); + + $addressId = $this->request->getRequiredBodyParam('addressId'); + $userId = $this->request->getRequiredBodyParam('userId'); + + $address = Address::find()->id($addressId)->one(); + + if (!$address) { + return $this->asFailure(Craft::t('commerce', 'Address not found.')); + } + + $user = Craft::$app->getUsers()->getUserById($userId); + + if (!$user || !$user->getIsCredentialed()) { + return $this->asFailure(Craft::t('commerce', 'Invalid user.')); + } + + try { + // Clone the address + $newAddress = Craft::$app->getElements()->duplicateElement($address, [ + 'owner' => $user, + 'primaryOwner' => $user, + ]); + } catch (\Exception $exception) { + return $this->asFailure($exception->getMessage()); + } + + return $this->asSuccess(data: [ + 'address' => $newAddress->toArray(), + ]); + } + /** * @throws BadRequestHttpException * @throws InvalidConfigException @@ -1026,9 +1066,7 @@ public function actionGetIndexSourcesBadgeCounts(): Response $counts = Plugin::getInstance()->getOrderStatuses()->getOrderCountByStatus($storeId); - $total = array_reduce($counts, static function($sum, $thing) { - return $sum + (int)$thing['orderCount']; - }, 0); + $total = array_reduce($counts, static fn($sum, $thing) => $sum + (int)$thing['orderCount'], 0); return $this->asSuccess(data: compact('counts', 'total')); } @@ -1556,11 +1594,11 @@ private function _updateOrder(Order $order, $orderRequestData, bool $tryAutoSet $shippingAddress = $getAddress($submittedShippingAddress, $order, Craft::t('commerce', 'Shipping Address')); $order->setShippingAddress($shippingAddress); - if (isset($orderRequestData['order']['sourceBillingAddressId'])) { + if (array_key_exists('sourceBillingAddressId',$orderRequestData['order'])) { $order->sourceBillingAddressId = $orderRequestData['order']['sourceBillingAddressId']; } - if (isset($orderRequestData['order']['sourceShippingAddressId'])) { + if (array_key_exists('sourceShippingAddressId',$orderRequestData['order'])) { $order->sourceShippingAddressId = $orderRequestData['order']['sourceShippingAddressId']; } } diff --git a/src/controllers/PaymentsController.php b/src/controllers/PaymentsController.php index cf0e21843d..01ed40a133 100644 --- a/src/controllers/PaymentsController.php +++ b/src/controllers/PaymentsController.php @@ -293,7 +293,7 @@ public function actionPay(): ?Response // Last line of try block we have a successful payment source creation $sourceCreated = true; - } catch (PaymentSourceCreatedLaterException $exception) { + } catch (PaymentSourceCreatedLaterException) { if (property_exists($paymentForm, 'paymentSource')) { $paymentForm->savePaymentSource = true; } diff --git a/src/controllers/SalesController.php b/src/controllers/SalesController.php index 3f18c05431..49df5538f2 100644 --- a/src/controllers/SalesController.php +++ b/src/controllers/SalesController.php @@ -31,7 +31,6 @@ use yii\web\HttpException; use yii\web\Response; use function explode; -use function get_class; /** * Class Sales Controller @@ -544,8 +543,8 @@ private function _populateVariables(&$variables): void foreach ($purchasableIds as $purchasableId) { $purchasable = Craft::$app->getElements()->getElementById((int)$purchasableId); if ($purchasable instanceof PurchasableInterface) { - $class = get_class($purchasable); - $purchasables[$class] = $purchasables[$class] ?? []; + $class = $purchasable::class; + $purchasables[$class] ??= []; $purchasables[$class][] = $purchasable; } } diff --git a/src/controllers/SettingsController.php b/src/controllers/SettingsController.php index 4efd5b367c..5a49985812 100644 --- a/src/controllers/SettingsController.php +++ b/src/controllers/SettingsController.php @@ -85,12 +85,10 @@ public function actionSites(): Response 'sites' => $sites, 'primaryStoreId' => Plugin::getInstance()->getStores()->getPrimaryStore()->id, 'stores' => Plugin::getInstance()->getStores()->getAllStores(), - 'storesList' => Plugin::getInstance()->getStores()->getAllStores()->map(function($store) { - return [ - 'label' => $store->name . ($store->primary ? ' (' . Craft::t('commerce', 'Primary') . ')' : ''), - 'value' => $store->id, - ]; - }), + 'storesList' => Plugin::getInstance()->getStores()->getAllStores()->map(fn($store) => [ + 'label' => $store->name . ($store->primary ? ' (' . Craft::t('commerce', 'Primary') . ')' : ''), + 'value' => $store->id, + ]), ]); } diff --git a/src/controllers/ShippingCategoriesController.php b/src/controllers/ShippingCategoriesController.php index b3bd606879..0d27d8d158 100644 --- a/src/controllers/ShippingCategoriesController.php +++ b/src/controllers/ShippingCategoriesController.php @@ -65,7 +65,7 @@ public function actionEdit(?string $storeHandle = null, int $id = null, Shipping $store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle); } - $store = $store ?? Plugin::getInstance()->getStores()->getPrimaryStore(); + $store ??= Plugin::getInstance()->getStores()->getPrimaryStore(); if (!$variables['shippingCategory']) { if ($variables['id']) { @@ -94,9 +94,7 @@ public function actionEdit(?string $storeHandle = null, int $id = null, Shipping $variables['productTypesOptions'] = []; if (!empty($variables['productTypes'])) { - $variables['productTypesOptions'] = ArrayHelper::map($variables['productTypes'], 'id', function($row) { - return ['label' => $row->name, 'value' => $row->id]; - }); + $variables['productTypesOptions'] = ArrayHelper::map($variables['productTypes'], 'id', fn($row) => ['label' => $row->name, 'value' => $row->id]); } $allShippingCategories = Plugin::getInstance()->getShippingCategories()->getAllShippingCategories($store->id); diff --git a/src/controllers/ShippingMethodsController.php b/src/controllers/ShippingMethodsController.php index bbe3403d05..9b2ada9723 100644 --- a/src/controllers/ShippingMethodsController.php +++ b/src/controllers/ShippingMethodsController.php @@ -102,6 +102,7 @@ public function actionSave(): ?Response $shippingMethod->handle = $this->request->getBodyParam('handle'); $shippingMethod->storeId = $this->request->getBodyParam('storeId'); $shippingMethod->setOrderCondition($this->request->getBodyParam('orderCondition')); + $shippingMethod->setCustomerCondition($this->request->getBodyParam('customerCondition')); $shippingMethod->enabled = (bool)$this->request->getBodyParam('enabled'); // Save it diff --git a/src/controllers/TaxCategoriesController.php b/src/controllers/TaxCategoriesController.php index d9cdbe5b87..10297a53a1 100644 --- a/src/controllers/TaxCategoriesController.php +++ b/src/controllers/TaxCategoriesController.php @@ -83,9 +83,7 @@ public function actionEdit(?string $storeHandle = null, int $id = null, TaxCateg $variables['productTypesOptions'] = []; if (!empty($variables['productTypes'])) { - $variables['productTypesOptions'] = ArrayHelper::map($variables['productTypes'], 'id', function($row) { - return ['label' => $row->name, 'value' => $row->id]; - }); + $variables['productTypesOptions'] = ArrayHelper::map($variables['productTypes'], 'id', fn($row) => ['label' => $row->name, 'value' => $row->id]); } $allTaxCategoryIds = array_keys(Plugin::getInstance()->getTaxCategories()->getAllTaxCategories()); diff --git a/src/controllers/TaxRatesController.php b/src/controllers/TaxRatesController.php index 777b2bfbcd..babaaf34db 100644 --- a/src/controllers/TaxRatesController.php +++ b/src/controllers/TaxRatesController.php @@ -171,9 +171,7 @@ public function actionEdit(?string $storeHandle = null, int $id = null, TaxRate $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); $productTypesOptions = []; if (!empty($productTypes)) { - $productTypesOptions = ArrayHelper::map($productTypes, 'id', function(ProductType $row) { - return ['label' => $row->name, 'value' => $row->id]; - }); + $productTypesOptions = ArrayHelper::map($productTypes, 'id', fn(ProductType $row) => ['label' => $row->name, 'value' => $row->id]); } $variables['newTaxCategoryFields'] = $view->namespaceInputs( $view->renderTemplate('commerce/store-management/tax/taxcategories/_fields', compact('productTypes', 'productTypesOptions')) diff --git a/src/controllers/TransfersController.php b/src/controllers/TransfersController.php index 1bcfc06c1c..b5ba742a7e 100644 --- a/src/controllers/TransfersController.php +++ b/src/controllers/TransfersController.php @@ -326,9 +326,7 @@ public function actionRenderManagement(): string $details = $this->request->getParam('details', []); if ($this->request->getParam('removeInventoryItemUid')) { - $details = array_filter($details, function($detail) { - return $detail['uid'] !== $this->request->getParam('removeInventoryItemUid'); - }); + $details = array_filter($details, fn($detail) => $detail['uid'] !== $this->request->getParam('removeInventoryItemUid')); } $transfer->setDetails($details); diff --git a/src/elements/Donation.php b/src/elements/Donation.php index 90c8fd8f3e..e7f8568a5a 100644 --- a/src/elements/Donation.php +++ b/src/elements/Donation.php @@ -72,10 +72,9 @@ protected function defineRules(): array $rules[] = [['sku'], 'trim']; $rules[] = [ - ['sku'], 'required', 'when' => function($model) { + ['sku'], 'required', 'when' => fn($model) => /** @var self $model */ - return $model->availableForPurchase && $model->enabled; - }, + $model->availableForPurchase && $model->enabled, ]; return $rules; diff --git a/src/elements/Order.php b/src/elements/Order.php index 11e213aac8..8f093e8249 100644 --- a/src/elements/Order.php +++ b/src/elements/Order.php @@ -26,6 +26,8 @@ use craft\commerce\elements\traits\OrderNoticesTrait; use craft\commerce\elements\traits\OrderValidatorsTrait; use craft\commerce\errors\CurrencyException; +use craft\commerce\errors\LineItemNotFoundException; +use craft\commerce\errors\OrderAdjustmentNotFoundException; use craft\commerce\errors\OrderStatusException; use craft\commerce\events\AddLineItemEvent; use craft\commerce\events\LineItemEvent; @@ -46,7 +48,6 @@ use craft\commerce\Plugin; use craft\commerce\records\LineItem as LineItemRecord; use craft\commerce\records\Order as OrderRecord; -use craft\commerce\records\OrderAdjustment as OrderAdjustmentRecord; use craft\commerce\records\OrderNotice as OrderNoticeRecord; use craft\commerce\records\Transaction as TransactionRecord; use craft\db\Query; @@ -55,6 +56,7 @@ use craft\errors\DeprecationException; use craft\errors\ElementNotFoundException; use craft\errors\InvalidElementException; +use craft\errors\MutexException; use craft\errors\UnsupportedSiteException; use craft\fields\BaseRelationField; use craft\helpers\ArrayHelper; @@ -1563,10 +1565,9 @@ protected function defineRules(): array // Are the addresses both being set to each other. [ ['billingAddress', 'shippingAddress'], 'validateAddressReuse', - 'when' => function($model) { + 'when' => fn($model) => /** @var Order $model */ - return !$model->isCompleted; - }, + !$model->isCompleted, ], [['shippingAddress'], 'validateOrganizationTaxIdAsVatId', 'when' => fn(Order $order) => $order->getStore()->getValidateOrganizationTaxIdAsVatId() && !$order->getStore()->getUseBillingAddressForTax()], @@ -1815,7 +1816,7 @@ public function markAsComplete(): bool try { $baseReference = Craft::$app->getView()->renderObjectTemplate($referenceTemplate, $this); - + // Check if this reference already exists and append suffix if needed $suffix = 0; $testReference = $baseReference; @@ -1826,13 +1827,13 @@ public function markAsComplete(): bool ->from([Table::ORDERS]) ->where(['reference' => $testReference]) ->exists(); - + if (!$existingReference) { // Reference is unique, use it $this->reference = $testReference; break; } - + // Reference exists, increment suffix and try again $suffix++; $testReference = $baseReference . '-' . $suffix; @@ -2197,170 +2198,182 @@ public function getAvailableShippingMethodOptions(): array */ public function afterSave(bool $isNew): void { - // Make sure addresses are set before recalculation so that on the next page load - // the correct adjustments and totals are shown - if ($this->shippingSameAsBilling) { - $this->setShippingAddress($this->getBillingAddress()); + $lockKey = "order-after-save:$this->number"; + $mutex = Craft::$app->getMutex(); + if (!$mutex->acquire($lockKey, 15)) { + throw new MutexException($lockKey, 'Could not acquire a lock to save the order.'); } - if ($this->billingSameAsShipping) { - $this->setBillingAddress($this->getShippingAddress()); - } + try { + + // Make sure addresses are set before recalculation so that on the next page load + // the correct adjustments and totals are shown + if ($this->shippingSameAsBilling) { + $this->setShippingAddress($this->getBillingAddress()); + } - // TODO: Move the recalculate to somewhere else. Saving should be for saving only #COM-40 - // Right now orders always recalc when saved and not completed but that shouldn't always be the case. - $this->recalculate(); + if ($this->billingSameAsShipping) { + $this->setBillingAddress($this->getShippingAddress()); + } + + // TODO: Move the recalculate to somewhere else. Saving should be for saving only #COM-40 + // Right now orders always recalc when saved and not completed but that shouldn't always be the case. + $this->recalculate(); - if (!$isNew) { - $orderRecord = OrderRecord::findOne($this->id); + if (!$isNew) { + $orderRecord = OrderRecord::findOne($this->id); - if (!$orderRecord) { - throw new Exception('Invalid order ID: ' . $this->id); + if (!$orderRecord) { + throw new Exception('Invalid order ID: ' . $this->id); + } + } else { + $orderRecord = new OrderRecord(); + $orderRecord->id = $this->id; } - } else { - $orderRecord = new OrderRecord(); - $orderRecord->id = $this->id; - } - - $oldStatusId = $orderRecord->orderStatusId; - - $orderRecord->storeId = $this->storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; - $orderRecord->number = $this->number; - $orderRecord->reference = $this->reference; - $orderRecord->itemTotal = $this->getItemTotal(); - $orderRecord->itemSubtotal = $this->getItemSubtotal(); - $orderRecord->email = $this->getEmail() ?: ''; - $orderRecord->orderCompletedEmail = $this->orderCompletedEmail; - $orderRecord->isCompleted = $this->isCompleted; - - $dateOrdered = $this->dateOrdered; - if (!$dateOrdered && $orderRecord->isCompleted) { - $dateOrdered = Db::prepareDateForDb(new DateTime()); - } - $orderRecord->dateOrdered = $dateOrdered; - - $orderRecord->datePaid = $this->datePaid ?: null; - $orderRecord->dateAuthorized = $this->dateAuthorized ?: null; - $orderRecord->shippingMethodHandle = $this->shippingMethodHandle ?? ''; - $orderRecord->shippingMethodName = $this->shippingMethodName ?? ''; - $orderRecord->paymentSourceId = $this->getPaymentSource() ? $this->getPaymentSource()->id : null; - $orderRecord->gatewayId = $this->gatewayId; - $orderRecord->orderStatusId = $this->orderStatusId; - $orderRecord->couponCode = $this->couponCode; - $orderRecord->total = $this->getTotal(); - $orderRecord->totalPrice = $this->getTotalPrice(); - $orderRecord->totalPaid = $this->getTotalPaid(); - $orderRecord->totalDiscount = $this->getTotalDiscount(); - $orderRecord->totalShippingCost = $this->getTotalShippingCost(); - $orderRecord->totalTax = $this->getTotalTax(); - $orderRecord->totalTaxIncluded = $this->getTotalTaxIncluded(); - $orderRecord->totalQty = $this->getTotalQty(); - $orderRecord->totalWeight = $this->getTotalWeight(); - $orderRecord->currency = $this->currency; - $orderRecord->lastIp = $this->lastIp; - $orderRecord->orderLanguage = $this->orderLanguage; - $orderRecord->orderSiteId = $this->orderSiteId; - $orderRecord->origin = $this->origin; - $orderRecord->paymentCurrency = $this->paymentCurrency; - $orderRecord->customerId = $this->getCustomerId(); - $orderRecord->registerUserOnOrderComplete = $this->registerUserOnOrderComplete; - $orderRecord->saveBillingAddressOnOrderComplete = $this->saveBillingAddressOnOrderComplete; - $orderRecord->saveShippingAddressOnOrderComplete = $this->saveShippingAddressOnOrderComplete; - $orderRecord->returnUrl = $this->returnUrl; - $orderRecord->cancelUrl = $this->cancelUrl; - $orderRecord->message = $this->message; - $orderRecord->paidStatus = $this->getPaidStatus(); - $orderRecord->recalculationMode = $this->getRecalculationMode(); - $orderRecord->sourceShippingAddressId = $this->sourceShippingAddressId; - $orderRecord->sourceBillingAddressId = $this->sourceBillingAddressId; - $orderRecord->makePrimaryShippingAddress = $this->makePrimaryShippingAddress; - $orderRecord->makePrimaryBillingAddress = $this->makePrimaryBillingAddress; - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $orderRecord->dateUpdated = $this->dateUpdated; - $orderRecord->dateCreated = $this->dateCreated; - - $currentUser = Craft::$app->getUser()->getIdentity(); - $currentUserIsCustomer = ($currentUser && $this->getCustomer() && $currentUser->id == $this->getCustomer()->id); - - if ($shippingAddress = $this->getShippingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $shippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - $shippingAddress->title = Craft::t('commerce', 'Shipping Address'); // Ensure the address is labelled correctly - Craft::$app->getElements()->saveElement($shippingAddress, false); - $orderRecord->shippingAddressId = $shippingAddress->id; - $this->setShippingAddress($shippingAddress); - // Set primary shipping if asked - if ($this->makePrimaryShippingAddress && $currentUserIsCustomer && $this->sourceShippingAddressId) { - Plugin::getInstance()->getCustomers()->savePrimaryShippingAddressId($this->getCustomer(), $this->sourceShippingAddressId); + + $oldStatusId = $orderRecord->orderStatusId; + + $orderRecord->storeId = $this->storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $orderRecord->number = $this->number; + $orderRecord->reference = $this->reference; + $orderRecord->itemTotal = $this->getItemTotal(); + $orderRecord->itemSubtotal = $this->getItemSubtotal(); + $orderRecord->email = $this->getEmail() ?: ''; + $orderRecord->orderCompletedEmail = $this->orderCompletedEmail; + $orderRecord->isCompleted = $this->isCompleted; + + $dateOrdered = $this->dateOrdered; + if (!$dateOrdered && $orderRecord->isCompleted) { + $dateOrdered = Db::prepareDateForDb(new DateTime()); + } + $orderRecord->dateOrdered = $dateOrdered; + + $orderRecord->datePaid = $this->datePaid ?: null; + $orderRecord->dateAuthorized = $this->dateAuthorized ?: null; + $orderRecord->shippingMethodHandle = $this->shippingMethodHandle ?? ''; + $orderRecord->shippingMethodName = $this->shippingMethodName ?? ''; + $orderRecord->paymentSourceId = $this->getPaymentSource() ? $this->getPaymentSource()->id : null; + $orderRecord->gatewayId = $this->gatewayId; + $orderRecord->orderStatusId = $this->orderStatusId; + $orderRecord->couponCode = $this->couponCode; + $orderRecord->total = $this->getTotal(); + $orderRecord->totalPrice = $this->getTotalPrice(); + $orderRecord->totalPaid = $this->getTotalPaid(); + $orderRecord->totalDiscount = $this->getTotalDiscount(); + $orderRecord->totalShippingCost = $this->getTotalShippingCost(); + $orderRecord->totalTax = $this->getTotalTax(); + $orderRecord->totalTaxIncluded = $this->getTotalTaxIncluded(); + $orderRecord->totalQty = $this->getTotalQty(); + $orderRecord->totalWeight = $this->getTotalWeight(); + $orderRecord->currency = $this->currency; + $orderRecord->lastIp = $this->lastIp; + $orderRecord->orderLanguage = $this->orderLanguage; + $orderRecord->orderSiteId = $this->orderSiteId; + $orderRecord->origin = $this->origin; + $orderRecord->paymentCurrency = $this->paymentCurrency; + $orderRecord->customerId = $this->getCustomerId(); + $orderRecord->registerUserOnOrderComplete = $this->registerUserOnOrderComplete; + $orderRecord->saveBillingAddressOnOrderComplete = $this->saveBillingAddressOnOrderComplete; + $orderRecord->saveShippingAddressOnOrderComplete = $this->saveShippingAddressOnOrderComplete; + $orderRecord->returnUrl = $this->returnUrl; + $orderRecord->cancelUrl = $this->cancelUrl; + $orderRecord->message = $this->message; + $orderRecord->paidStatus = $this->getPaidStatus(); + $orderRecord->recalculationMode = $this->getRecalculationMode(); + $orderRecord->sourceShippingAddressId = $this->sourceShippingAddressId; + $orderRecord->sourceBillingAddressId = $this->sourceBillingAddressId; + $orderRecord->makePrimaryShippingAddress = $this->makePrimaryShippingAddress; + $orderRecord->makePrimaryBillingAddress = $this->makePrimaryBillingAddress; + + // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving + $orderRecord->dateUpdated = $this->dateUpdated; + $orderRecord->dateCreated = $this->dateCreated; + + $currentUser = Craft::$app->getUser()->getIdentity(); + $currentUserIsCustomer = ($currentUser && $this->getCustomer() && $currentUser->id == $this->getCustomer()->id); + + if ($shippingAddress = $this->getShippingAddress()) { + // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error + // This is because the order record has not been saved. + // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. + $shippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order + $shippingAddress->title = Craft::t('commerce', 'Shipping Address'); // Ensure the address is labelled correctly + Craft::$app->getElements()->saveElement($shippingAddress, false); + $orderRecord->shippingAddressId = $shippingAddress->id; + $this->setShippingAddress($shippingAddress); + // Set primary shipping if asked + if ($this->makePrimaryShippingAddress && $currentUserIsCustomer && $this->sourceShippingAddressId) { + Plugin::getInstance()->getCustomers()->savePrimaryShippingAddressId($this->getCustomer(), $this->sourceShippingAddressId); + } + } else { + $orderRecord->shippingAddressId = null; + $this->setShippingAddress(null); } - } else { - $orderRecord->shippingAddressId = null; - $this->setShippingAddress(null); - } - if ($billingAddress = $this->getBillingAddress()) { - // If these were set to the same address element, we don't want the same address IDs - if ($shippingAddress && $billingAddress->id == $shippingAddress->id) { - $billingAddress = Craft::$app->getElements()->duplicateElement( - $billingAddress, - ['primaryOwner' => $this, 'title' => Craft::t('commerce', 'Billing Address')] - ); + if ($billingAddress = $this->getBillingAddress()) { + // If these were set to the same address element, we don't want the same address IDs + if ($shippingAddress && $billingAddress->id == $shippingAddress->id) { + $billingAddress = Craft::$app->getElements()->duplicateElement($billingAddress, + ['owner' => $this, 'title' => Craft::t('commerce', 'Billing Address')]); + } else { + // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error + // This is because the order record has not been saved. + // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. + $billingAddress->setOwner($this); // Always ensure the address is owned by the order + $billingAddress->title = Craft::t('commerce', 'Billing Address'); // Ensure the address is labelled correctly + Craft::$app->getElements()->saveElement($billingAddress, false); + } + + $orderRecord->billingAddressId = $billingAddress->id; + $this->setBillingAddress($billingAddress); + // Set primary billing if asked + if ($this->makePrimaryBillingAddress && $currentUserIsCustomer && $this->sourceBillingAddressId) { + Plugin::getInstance()->getCustomers()->savePrimaryBillingAddressId($this->getCustomer(), $this->sourceBillingAddressId); + } } else { + $orderRecord->billingAddressId = null; + $this->setBillingAddress(null); + } + + if ($estimatedShippingAddress = $this->getEstimatedShippingAddress()) { // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error // This is because the order record has not been saved. // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $billingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - $billingAddress->title = Craft::t('commerce', 'Billing Address'); // Ensure the address is labelled correctly - Craft::$app->getElements()->saveElement($billingAddress, false); + $estimatedShippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order + Craft::$app->getElements()->saveElement($estimatedShippingAddress, false); + $orderRecord->estimatedShippingAddressId = $estimatedShippingAddress->id; + $this->setEstimatedShippingAddress($estimatedShippingAddress); + + // If estimate billing same as shipping set it here + if ($this->estimatedBillingSameAsShipping) { + $orderRecord->estimatedBillingAddressId = $estimatedShippingAddress->id; + $this->setEstimatedBillingAddress($estimatedShippingAddress); + } } - $orderRecord->billingAddressId = $billingAddress->id; - $this->setBillingAddress($billingAddress); - // Set primary billing if asked - if ($this->makePrimaryBillingAddress && $currentUserIsCustomer && $this->sourceBillingAddressId) { - Plugin::getInstance()->getCustomers()->savePrimaryBillingAddressId($this->getCustomer(), $this->sourceBillingAddressId); - } - } else { - $orderRecord->billingAddressId = null; - $this->setBillingAddress(null); - } - - if ($estimatedShippingAddress = $this->getEstimatedShippingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $estimatedShippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - Craft::$app->getElements()->saveElement($estimatedShippingAddress, false); - $orderRecord->estimatedShippingAddressId = $estimatedShippingAddress->id; - $this->setEstimatedShippingAddress($estimatedShippingAddress); - - // If estimate billing same as shipping set it here - if ($this->estimatedBillingSameAsShipping) { - $orderRecord->estimatedBillingAddressId = $estimatedShippingAddress->id; - $this->setEstimatedBillingAddress($estimatedShippingAddress); + if (!$this->estimatedBillingSameAsShipping && $estimatedBillingAddress = $this->getEstimatedBillingAddress()) { + // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error + // This is because the order record has not been saved. + // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. + $estimatedBillingAddress->setOwner($this); // Always ensure the address is owned by the order + Craft::$app->getElements()->saveElement($estimatedBillingAddress, false); + $orderRecord->estimatedBillingAddressId = $estimatedBillingAddress->id; + $this->setEstimatedBillingAddress($estimatedBillingAddress); } - } - if (!$this->estimatedBillingSameAsShipping && $estimatedBillingAddress = $this->getEstimatedBillingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $estimatedBillingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - Craft::$app->getElements()->saveElement($estimatedBillingAddress, false); - $orderRecord->estimatedBillingAddressId = $estimatedBillingAddress->id; - $this->setEstimatedBillingAddress($estimatedBillingAddress); - } + $orderRecord->save(false); - $orderRecord->save(false); + $this->_saveAdjustments(); + $this->_saveLineItems(); + $this->_saveNotices(); + $this->_saveOrderHistory($oldStatusId, $orderRecord->orderStatusId); + $this->_deleteOrphanedOrderAddresses(); + } catch (Exception $exception) { + $mutex->release($lockKey); + throw $exception; + } - $this->_saveAdjustments(); - $this->_saveLineItems(); - $this->_saveNotices(); - $this->_saveOrderHistory($oldStatusId, $orderRecord->orderStatusId); - $this->_deleteOrphanedOrderAddresses(); + $mutex->release($lockKey); parent::afterSave($isNew); } @@ -2779,15 +2792,11 @@ public function getTotalPaid(): float $transactions = collect($this->_transactions); - $paid = $transactions->filter(function($transaction) { - return $transaction->status == TransactionRecord::STATUS_SUCCESS - && in_array($transaction->type, [TransactionRecord::TYPE_PURCHASE, TransactionRecord::TYPE_CAPTURE]); - })->sum('amount'); + $paid = $transactions->filter(fn($transaction) => $transaction->status == TransactionRecord::STATUS_SUCCESS + && in_array($transaction->type, [TransactionRecord::TYPE_PURCHASE, TransactionRecord::TYPE_CAPTURE]))->sum('amount'); - $refunded = $transactions->filter(function($transaction) { - return $transaction->status == TransactionRecord::STATUS_SUCCESS - && $transaction->type == TransactionRecord::TYPE_REFUND; - })->sum('amount'); + $refunded = $transactions->filter(fn($transaction) => $transaction->status == TransactionRecord::STATUS_SUCCESS + && $transaction->type == TransactionRecord::TYPE_REFUND)->sum('amount'); return (float)$this->getTeller()->subtract($paid, $refunded); } @@ -3293,10 +3302,9 @@ public function removeBillingAddress(): void public function hasMatchingAddresses(?array $attributes = null): bool { $addressAttributes = (new ReflectionClass(AddressInterface::class))->getMethods(); - $addressAttributes = array_map(static function(ReflectionMethod $method) { + $addressAttributes = array_map(static fn(ReflectionMethod $method) => // Remove `get` and lower case first character - return lcfirst(substr($method->name, 3)); - }, $addressAttributes); + lcfirst(substr($method->name, 3)), $addressAttributes); $relationCustomFieldHandles = []; $customFieldHandles = array_map(static function(FieldInterface $field) use (&$relationCustomFieldHandles) { @@ -3307,9 +3315,7 @@ public function hasMatchingAddresses(?array $attributes = null): bool return $field->handle; }, (new AddressElement())->getFieldLayout()->getCustomFields()); - $nameTraitProperties = array_map(static function(ReflectionProperty $property) { - return $property->name; - }, (new ReflectionClass(NameTrait::class))->getProperties()); + $nameTraitProperties = array_map(static fn(ReflectionProperty $property) => $property->name, (new ReflectionClass(NameTrait::class))->getProperties()); $toArrayHandles = [...$nameTraitProperties, ...$addressAttributes, ...$customFieldHandles]; @@ -3631,26 +3637,22 @@ public function getMetadata(): array */ private function _saveAdjustments(): void { - /** @var null|array|OrderAdjustmentRecord[] $previousAdjustments */ - $previousAdjustments = OrderAdjustmentRecord::find() - ->where(['orderId' => $this->id]) - ->all(); - $newAdjustmentIds = []; foreach ($this->getAdjustments() as $adjustment) { - // Don't run validation as validation of the adjustments should happen before saving the order - Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); + try { + // Don't run validation as validation of the adjustment should happen before saving the order + Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); + } catch (OrderAdjustmentNotFoundException) { + // If the adjustment was not found, it means it may have previously existed but was already deleted (race condition). + // See: https://github.com/craftcms/commerce/issues/3283 + continue; + } + $newAdjustmentIds[] = $adjustment->id; $adjustment->orderId = $this->id; } - foreach ($previousAdjustments as $previousAdjustment) { - if (!in_array($previousAdjustment->id, $newAdjustmentIds, false)) { - $previousAdjustment->delete(); - } - } - // Make sure all other adjustments have been cleaned up. Db::delete( Table::ORDERADJUSTMENTS, @@ -3749,8 +3751,14 @@ private function _saveLineItems(): void $originalId = $lineItem->id; $lineItem->setOrder($this); // just in case. - // Don't run validation as validation of the line item should happen before saving the order - Plugin::getInstance()->getLineItems()->saveLineItem($lineItem, false); + try { + // Don't run validation as validation of the line item should happen before saving the order + Plugin::getInstance()->getLineItems()->saveLineItem($lineItem, false); + } catch (LineItemNotFoundException) { + // If the line item was not found, it means it may have previously existed but was already deleted (race condition). + // See: https://github.com/craftcms/commerce/issues/3283 + continue; + } // Is this a new line item? if ($originalId === null) { @@ -3770,7 +3778,12 @@ private function _saveLineItems(): void // Re-save the adjustment with the new line item ID, since it exists now. $adjustment->lineItemId = $lineItem->id; // Validation not needed as the adjustments are validated before the order is saved - Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); + try { + Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); + } catch (OrderAdjustmentNotFoundException) { + // This can happen if the adjustment was removed during a race condition recalculation. + continue; + } } } } diff --git a/src/elements/Product.php b/src/elements/Product.php index d681d356f6..557517da38 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -77,6 +77,7 @@ * @property Variant[]|array $variants an array of the product's variants * @property-read string $defaultPriceAsCurrency * @property-read string $defaultBasePriceAsCurrency + * @property-read string $defaultBasePromotionalPriceAsCurrency * @property float|null $defaultPrice * @author Pixel & Tonic, Inc. * @since 2.0 @@ -96,12 +97,6 @@ class Product extends Element implements HasStoreInterface */ public const EVENT_DEFINE_PARENT_SELECTION_CRITERIA = 'defineParentSelectionCriteria'; - /** - * @var float|null - * @since 5.1.0 - */ - public ?float $defaultBasePrice = null; - /** * @inheritdoc */ @@ -535,6 +530,7 @@ protected static function defineTableAttributes(): array 'dateCreated' => ['label' => Craft::t('commerce', 'Date Created')], 'dateUpdated' => ['label' => Craft::t('commerce', 'Date Updated')], 'defaultPrice' => ['label' => Craft::t('commerce', 'Price')], + 'defaultPromotionalPrice' => ['label' => Craft::t('commerce', 'Promotional Price')], 'defaultSku' => ['label' => Craft::t('commerce', 'SKU')], 'defaultWeight' => ['label' => Craft::t('commerce', 'Weight')], 'defaultLength' => ['label' => Craft::t('commerce', 'Length')], @@ -586,6 +582,10 @@ protected static function defineCardAttributes(): array 'label' => Craft::t('commerce', 'Price'), 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), ], + 'defaultPromotionalPrice' => [ + 'label' => Craft::t('commerce', 'Promotional Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], 'defaultSku' => [ 'label' => Craft::t('commerce', 'SKU'), 'placeholder' => Html::tag('code', 'SKU123'), @@ -652,7 +652,18 @@ public static function gqlScopesByContext(mixed $context): array */ public static function prepElementQueryForTableAttribute(ElementQueryInterface $elementQuery, string $attribute): void { - if ($attribute === 'variants') { + $variantAttributes = [ + 'variants', + 'defaultPrice', + 'defaultPromotionalPrice', + 'defaultSku', + 'defaultWeight', + 'defaultLength', + 'defaultWidth', + 'defaultHeight', + ]; + + if (in_array($attribute, $variantAttributes, false)) { $elementQuery->andWith('variants'); } else { parent::prepElementQueryForTableAttribute($elementQuery, $attribute); @@ -691,6 +702,18 @@ public static function prepElementQueryForTableAttribute(ElementQueryInterface $ */ private ?float $_defaultPrice = null; + /** + * @var float|null + * @since 5.1.0 + */ + public ?float $defaultBasePrice = null; + + /** + * @var float|null + * @since 5.4.0 + */ + public ?float $defaultBasePromotionalPrice = null; + /** * @var float|null Default height */ @@ -728,6 +751,7 @@ public static function prepElementQueryForTableAttribute(ElementQueryInterface $ /** * @var NestedElementManager|null + * @see getVariantManager() * @since 5.0.0 */ private ?NestedElementManager $_variantManager = null; @@ -738,7 +762,7 @@ public static function prepElementQueryForTableAttribute(ElementQueryInterface $ */ public function currencyAttributes(): array { - return ['defaultPrice', 'defaultBasePrice']; + return ['defaultPrice', 'defaultBasePrice', 'defaultBasePromotionalPrice']; } /** @@ -1909,7 +1933,11 @@ protected function attributeHtml(string $attribute): string } case 'defaultPrice': { - return $this->defaultBasePriceAsCurrency; + return $this->defaultBasePrice ? $this->defaultBasePriceAsCurrency : ''; + } + case 'defaultPromotionalPrice': + { + return $this->defaultBasePromotionalPrice ? $this->defaultBasePromotionalPriceAsCurrency : ''; } case 'stock': { @@ -1951,9 +1979,7 @@ protected function attributeHtml(string $attribute): string if ($value->isNotEmpty() && $value->count() > 1) { $otherItems = $value->filter(fn($v, $k) => $k > 0); - $otherHtml = $otherItems->map(function($v) { - return Cp::elementChipHtml($v); - })->join(''); + $otherHtml = $otherItems->map(fn($v) => Cp::elementChipHtml($v))->join(''); $html .= Html::tag('span', '+' . Craft::$app->getFormatter()->asInteger($otherItems->count()), [ 'title' => $otherItems->map(fn($v) => $v->title)->join(', '), diff --git a/src/elements/Subscription.php b/src/elements/Subscription.php index fdc5a37718..71cb718aaa 100644 --- a/src/elements/Subscription.php +++ b/src/elements/Subscription.php @@ -738,15 +738,10 @@ protected static function defineSortOptions(): array */ protected static function prepElementQueryForTableAttribute(ElementQueryInterface $elementQuery, string $attribute): void { - switch ($attribute) { - case 'subscriber': - $elementQuery->andWith('subscriber'); - break; - case 'orderLink': - $elementQuery->andWith('order'); - break; - default: - parent::prepElementQueryForTableAttribute($elementQuery, $attribute); - } + match ($attribute) { + 'subscriber' => $elementQuery->andWith('subscriber'), + 'orderLink' => $elementQuery->andWith('order'), + default => parent::prepElementQueryForTableAttribute($elementQuery, $attribute), + }; } } diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php index 25ab053b4b..a56ff033d8 100644 --- a/src/elements/Transfer.php +++ b/src/elements/Transfer.php @@ -369,24 +369,12 @@ protected static function defineDefaultTableAttributes(string $source): array */ protected function attributeHtml(string $attribute): string { - switch ($attribute) { - case 'originLocation': - { - return $this->getOriginLocation()?->getUiLabel() ?? ''; - } - case 'destinationLocation': - { - return $this->getDestinationLocation()?->getUiLabel() ?? ''; - } - case 'received': - { - return $this->getTotalReceived() . '/' . $this->getTotalQuantity(); - } - default: - { - return parent::attributeHtml($attribute); - } - } + return match ($attribute) { + 'originLocation' => $this->getOriginLocation()?->getUiLabel() ?? '', + 'destinationLocation' => $this->getDestinationLocation()?->getUiLabel() ?? '', + 'received' => $this->getTotalReceived() . '/' . $this->getTotalQuantity(), + default => parent::attributeHtml($attribute), + }; } /** diff --git a/src/elements/Variant.php b/src/elements/Variant.php index 30eabd37c6..2e961cc170 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -31,6 +31,7 @@ use craft\commerce\records\Variant as VariantRecord; use craft\db\Query; use craft\db\Table as CraftTable; +use craft\elements\actions\Copy; use craft\elements\actions\Restore; use craft\elements\conditions\ElementConditionInterface; use craft\elements\db\EagerLoadPlan; @@ -325,6 +326,14 @@ public function canSave(User $user): bool return $product->canSave($user); } + /** + * @inheritdoc + */ + public function canCopy(User $user): bool + { + return true; + } + /** * @inheritdoc */ @@ -370,6 +379,14 @@ public function canDuplicate(User $user): bool return $this->canSave($user); } + /** + * @inheritdoc + */ + protected static function includeSetStatusAction(): bool + { + return true; + } + /** * @inheritdoc * @throws InvalidConfigException @@ -957,7 +974,6 @@ public function getGqlTypeName(): string } /** - * @param mixed $context * @return string * @since 3.1 */ @@ -1288,9 +1304,7 @@ protected function availableShippingCategories(): array // Limit to only those for this product type $categoryIds = collect(Plugin::getInstance()->getShippingCategories()->getShippingCategoriesByProductTypeId($productTypeId))->pluck('id')->toArray(); - $available = collect($allAvailableShippingCategories)->filter(function(ShippingCategory $category) use ($categoryIds) { - return in_array($category->id, $categoryIds); - }); + $available = collect($allAvailableShippingCategories)->filter(fn(ShippingCategory $category) => in_array($category->id, $categoryIds)); if ($available->isEmpty()) { return [Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($this->storeId)]; @@ -1314,9 +1328,7 @@ protected function availableTaxCategories(): array // Limit to only those for this product type $categoryIds = collect(Plugin::getInstance()->getTaxCategories()->getTaxCategoriesByProductTypeId($productTypeId))->pluck('id')->toArray(); - $available = collect($allAvailableTaxCategories)->filter(function(TaxCategory $category) use ($categoryIds) { - return in_array($category->id, $categoryIds); - }); + $available = collect($allAvailableTaxCategories)->filter(fn(TaxCategory $category) => in_array($category->id, $categoryIds)); if ($available->isEmpty()) { return [Plugin::getInstance()->getTaxCategories()->getDefaultTaxCategory()]; @@ -1357,6 +1369,7 @@ protected static function defineActions(string $source): array ]); $actions[] = ['type' => SetDefaultVariant::class]; + $actions[] = ['type' => Copy::class]; return $actions; } diff --git a/src/elements/VariantCollection.php b/src/elements/VariantCollection.php index 32cc77ad5e..c259c5d6b0 100644 --- a/src/elements/VariantCollection.php +++ b/src/elements/VariantCollection.php @@ -51,8 +51,6 @@ public static function make($items = []) */ public function cheapest(): ?Variant { - return $this->reduce(function(?Variant $cheapest, Variant $variant) { - return !$cheapest || $variant->getSalePrice() < $cheapest->getSalePrice() ? $variant : $cheapest; - }); + return $this->reduce(fn(?Variant $cheapest, Variant $variant) => !$cheapest || $variant->getSalePrice() < $cheapest->getSalePrice() ? $variant : $cheapest); } } diff --git a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php b/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php index b46ae79339..5edef57cac 100644 --- a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php +++ b/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php @@ -59,7 +59,7 @@ public function matchElement(ElementInterface $element): bool try { return (bool)$formulasService->evaluateCondition($formula, ['postalCode' => $postalCode], 'Postal code formula matching address'); - } catch (\Throwable $e) { + } catch (\Throwable) { Craft::error('Error evaluating postal code formula: ' . $formula,'commerce'); return false; } diff --git a/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php b/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php index eaf19b5db1..c8af209efa 100644 --- a/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php +++ b/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php @@ -24,11 +24,9 @@ class CatalogPricingRuleCustomerCondition extends UserCondition */ protected function selectableConditionRules(): array { - return array_filter(parent::selectableConditionRules(), static function($type) { - return !in_array($type, [ - LastLoginDateConditionRule::class, - SiteConditionRule::class, - ], true); - }); + return array_filter(parent::selectableConditionRules(), static fn($type) => !in_array($type, [ + LastLoginDateConditionRule::class, + SiteConditionRule::class, + ], true)); } } diff --git a/src/elements/conditions/customers/ShippingMethodCustomerCondition.php b/src/elements/conditions/customers/ShippingMethodCustomerCondition.php new file mode 100644 index 0000000000..b95bcacc43 --- /dev/null +++ b/src/elements/conditions/customers/ShippingMethodCustomerCondition.php @@ -0,0 +1,20 @@ + + * @since 5.4.0 + */ +class ShippingMethodCustomerCondition extends UserCondition +{ +} diff --git a/src/elements/conditions/customers/ShippingRuleCustomerCondition.php b/src/elements/conditions/customers/ShippingRuleCustomerCondition.php new file mode 100644 index 0000000000..93b2a70654 --- /dev/null +++ b/src/elements/conditions/customers/ShippingRuleCustomerCondition.php @@ -0,0 +1,20 @@ + + * @since 5.4.0 + */ +class ShippingRuleCustomerCondition extends UserCondition +{ +} diff --git a/src/elements/conditions/orders/GatewayOrderCondition.php b/src/elements/conditions/orders/GatewayOrderCondition.php new file mode 100644 index 0000000000..59ce77bed3 --- /dev/null +++ b/src/elements/conditions/orders/GatewayOrderCondition.php @@ -0,0 +1,28 @@ + + * @since 5.4.0 + */ +class GatewayOrderCondition extends OrderCondition +{ + public function getBuilderHtml($readOnly = false): string + { + if ($readOnly) { + return Html::disableInputs(fn() => parent::getBuilderHtml()); + } + return parent::getBuilderHtml(); + } +} diff --git a/src/elements/conditions/orders/OrderStatusConditionRule.php b/src/elements/conditions/orders/OrderStatusConditionRule.php index ff7053d5dc..69e3c9a6d9 100644 --- a/src/elements/conditions/orders/OrderStatusConditionRule.php +++ b/src/elements/conditions/orders/OrderStatusConditionRule.php @@ -54,9 +54,7 @@ public function modifyQuery(ElementQueryInterface $query): void $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses(); /** @var OrderQuery $query */ - $query->orderStatus($this->paramValue(function(string $value) use ($orderStatuses) { - return ArrayHelper::firstWhere($orderStatuses, 'uid', $value)?->handle; - })); + $query->orderStatus($this->paramValue(fn(string $value) => ArrayHelper::firstWhere($orderStatuses, 'uid', $value)?->handle)); } /** @@ -73,8 +71,6 @@ public function matchElement(ElementInterface $element): bool protected function options(): array { - return Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()->mapWithKeys(function($status) { - return [$status->uid => $status->name]; - })->all(); + return Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()->mapWithKeys(fn($status) => [$status->uid => $status->name])->all(); } } diff --git a/src/elements/conditions/orders/PaymentGatewayConditionRule.php b/src/elements/conditions/orders/PaymentGatewayConditionRule.php index b31f3b4547..b859fb8d0c 100644 --- a/src/elements/conditions/orders/PaymentGatewayConditionRule.php +++ b/src/elements/conditions/orders/PaymentGatewayConditionRule.php @@ -40,9 +40,7 @@ public function getExclusiveQueryParams(): array */ protected function options(): array { - return Plugin::getInstance()->getGateways()->getAllGateways()->mapWithKeys(function($gateway) { - return [$gateway->uid => $gateway->name]; - })->all(); + return Plugin::getInstance()->getGateways()->getAllGateways()->mapWithKeys(fn($gateway) => [$gateway->uid => $gateway->name])->all(); } /** diff --git a/src/elements/conditions/orders/ShippingMethodConditionRule.php b/src/elements/conditions/orders/ShippingMethodConditionRule.php index e475593b98..c20ed7407a 100644 --- a/src/elements/conditions/orders/ShippingMethodConditionRule.php +++ b/src/elements/conditions/orders/ShippingMethodConditionRule.php @@ -40,9 +40,7 @@ public function getExclusiveQueryParams(): array */ protected function options(): array { - return Plugin::getInstance()->getShippingMethods()->getAllShippingMethods()->mapWithKeys(function($method) { - return [$method->handle => $method->name]; - })->all(); + return Plugin::getInstance()->getShippingMethods()->getAllShippingMethods()->mapWithKeys(fn($method) => [$method->handle => $method->name])->all(); } /** diff --git a/src/elements/conditions/products/ProductTypeConditionRule.php b/src/elements/conditions/products/ProductTypeConditionRule.php index 7dc4008467..b93a2085f7 100644 --- a/src/elements/conditions/products/ProductTypeConditionRule.php +++ b/src/elements/conditions/products/ProductTypeConditionRule.php @@ -61,9 +61,7 @@ public function modifyQuery(ElementQueryInterface $query): void $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); /** @var string[] $value */ - $value = $this->paramValue(function(string $value) use ($productTypes) { - return ArrayHelper::firstWhere($productTypes, 'uid', $value)?->handle; - }); + $value = $this->paramValue(fn(string $value) => ArrayHelper::firstWhere($productTypes, 'uid', $value)?->handle); /** @var ProductQuery $query */ $query->type($value); diff --git a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php b/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php index 3cd0a6ef6b..1d8566dc69 100644 --- a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php +++ b/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php @@ -24,11 +24,9 @@ class CatalogPricingRulePurchasableCondition extends ElementCondition */ protected function selectableConditionRules(): array { - $types = array_filter(parent::selectableConditionRules(), static function($type) { - return !in_array($type, [ - SiteConditionRule::class, - ], true); - }); + $types = array_filter(parent::selectableConditionRules(), static fn($type) => !in_array($type, [ + SiteConditionRule::class, + ], true)); $types[] = PurchasableConditionRule::class; $types[] = SkuConditionRule::class; diff --git a/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php b/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php index 86727cae52..770ffbb126 100644 --- a/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php +++ b/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php @@ -45,7 +45,7 @@ public function modifyQuery(ElementQueryInterface $query): void public function matchElement(ElementInterface $element): bool { /** @var Purchasable $element */ - return $this->matchValue(get_class($element)); + return $this->matchValue($element::class); } /** diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index dc8ae30c41..e7cc370865 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -144,34 +144,17 @@ public function init(): void */ public function __set($name, $value) { - switch ($name) { - case 'type': - $this->type($value); - break; - case 'before': - $this->before($value); - break; - case 'after': - $this->after($value); - break; - case 'defaultHeight': - $this->defaultHeight($value); - break; - case 'defaultLength': - $this->defaultLength($value); - break; - case 'defaultWidth': - $this->defaultWidth($value); - break; - case 'defaultWeight': - $this->defaultWeight($value); - break; - case 'defaultSku': - $this->defaultSku($value); - break; - default: - parent::__set($name, $value); - } + match ($name) { + 'type' => $this->type($value), + 'before' => $this->before($value), + 'after' => $this->after($value), + 'defaultHeight' => $this->defaultHeight($value), + 'defaultLength' => $this->defaultLength($value), + 'defaultWidth' => $this->defaultWidth($value), + 'defaultWeight' => $this->defaultWeight($value), + 'defaultSku' => $this->defaultSku($value), + default => parent::__set($name, $value), + }; } /** @@ -771,6 +754,7 @@ protected function beforePrepare(): bool 'commerce_products.expiryDate', 'subquery.price as defaultPrice', 'purchasablesstores.basePrice as defaultBasePrice', + 'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice', 'commerce_products.defaultVariantId', 'purchasables.sku as defaultSku', 'purchasables.weight as defaultWeight', diff --git a/src/elements/db/PurchasableQuery.php b/src/elements/db/PurchasableQuery.php index 517278caaf..42e9ec6bd8 100755 --- a/src/elements/db/PurchasableQuery.php +++ b/src/elements/db/PurchasableQuery.php @@ -121,13 +121,10 @@ abstract class PurchasableQuery extends ElementQuery */ public function __set($name, $value) { - switch ($name) { - case 'shippingCategory': - $this->shippingCategory($value); - break; - default: - parent::__set($name, $value); - } + match ($name) { + 'shippingCategory' => $this->shippingCategory($value), + default => parent::__set($name, $value), + }; } /** @@ -195,7 +192,6 @@ public function availableForPurchase(?bool $value = true): static * ->one(); * ``` * - * @param mixed $value * @return static self reference */ public function sku(mixed $value): static @@ -705,6 +701,7 @@ protected function beforePrepare(): bool 'subquery.price', 'subquery.promotionalPrice as promotionalPrice', 'subquery.salePrice as salePrice', + 'catprice.catalogPricingRuleId as catalogPricingRuleId', 'inventoryitems.id as inventoryItemId', ]); @@ -712,6 +709,25 @@ protected function beforePrepare(): bool $this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); $this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); + // Retrieve the catalog pricing rule ID used to determine the price + $customerId = $this->forCustomer; + if ($customerId === null) { + $customerId = Craft::$app->getUser()->getIdentity()?->id; + } elseif ($customerId === false) { + $customerId = null; + } + $cprIdQuery = Plugin::getInstance() + ->getCatalogPricing() + ->createCatalogPricesQuery(userId: $customerId) + ->select([ + 'purchasableId', + 'storeId', + 'price', + new Expression('MIN([[catalogPricingRuleId]]) as [[catalogPricingRuleId]]'), + ]) + ->groupBy(['cp.purchasableId', 'cp.storeId', 'cp.price']); + $this->query->leftJoin(['catprice' => $cprIdQuery], '[[catprice.purchasableId]] = [[commerce_purchasables.id]] AND [[catprice.storeId]] = [[sitestores.storeId]] AND [[catprice.price]] = [[subquery.salePrice]]'); + $this->subQuery->addSelect([ 'catalogprices.price', 'catalogprices.promotionalPrice', diff --git a/src/elements/db/SubscriptionQuery.php b/src/elements/db/SubscriptionQuery.php index ca6e3caa28..b2a0420c4d 100644 --- a/src/elements/db/SubscriptionQuery.php +++ b/src/elements/db/SubscriptionQuery.php @@ -136,16 +136,11 @@ public function __construct(string $elementType, array $config = []) */ public function __set($name, $value) { - switch ($name) { - case 'user': - $this->user($value); - break; - case 'plan': - $this->plan($value); - break; - default: - parent::__set($name, $value); - } + match ($name) { + 'user' => $this->user($value), + 'plan' => $this->plan($value), + default => parent::__set($name, $value), + }; } /** @@ -176,7 +171,6 @@ public function __set($name, $value) * ->all(); * ``` * - * @param mixed $value * @return static self reference */ public function user(mixed $value): SubscriptionQuery @@ -223,7 +217,6 @@ public function user(mixed $value): SubscriptionQuery * ->all(); * ``` * - * @param mixed $value * @return static self reference */ public function plan(mixed $value): SubscriptionQuery diff --git a/src/elements/db/VariantQuery.php b/src/elements/db/VariantQuery.php index 52cc3631ef..e0ec9262e2 100755 --- a/src/elements/db/VariantQuery.php +++ b/src/elements/db/VariantQuery.php @@ -128,23 +128,13 @@ public function __construct($elementType, array $config = []) */ public function __set($name, $value) { - switch ($name) { - case 'product': - $this->product($value); - break; - case 'productId': - // Added due to the removal of the `$productId` property - $this->ownerId($value); - break; - case 'owner': - $this->owner($value); - break; - case 'primaryOwner': - $this->primaryOwner($value); - break; - default: - parent::__set($name, $value); - } + match ($name) { + 'product' => $this->product($value), + 'productId' => $this->ownerId($value), + 'owner' => $this->owner($value), + 'primaryOwner' => $this->primaryOwner($value), + default => parent::__set($name, $value), + }; } /** @@ -156,7 +146,6 @@ public function __set($name, $value) * | - | - * | a [[Product|Product]] object | for a product represented by the object. * - * @param mixed $value * @return static self reference */ public function product(mixed $value): VariantQuery @@ -178,7 +167,6 @@ public function product(mixed $value): VariantQuery * | - | - * | a [[Product|Product]] object | for a product represented by the object. * - * @param mixed $value * @return static self reference */ public function owner(mixed $value): VariantQuery @@ -200,7 +188,6 @@ public function owner(mixed $value): VariantQuery * | - | - * | a [[ElementInterface|ElementInterface]] object | for a product represented by the object. * - * @param mixed $value * @return static self reference */ public function primaryOwner(mixed $value): VariantQuery @@ -224,7 +211,6 @@ public function primaryOwner(mixed $value): VariantQuery * | `[1, 2]` | for product with an ID of 1 or 2. * | `['not', 1, 2]` | for product not with an ID of 1 or 2. * - * @param mixed $value * @return static self reference */ public function productId(mixed $value): VariantQuery @@ -244,7 +230,6 @@ public function productId(mixed $value): VariantQuery * | `[1, 2]` | for primary owner with an ID of 1 or 2. * | `['not', 1, 2]` | for primary owner not with an ID of 1 or 2. * - * @param mixed $value * @return static self reference */ public function primaryOwnerId(mixed $value): VariantQuery @@ -264,7 +249,6 @@ public function primaryOwnerId(mixed $value): VariantQuery * | `[1, 2]` | for owner with an ID of 1 or 2. * | `['not', 1, 2]` | for owner not with an ID of 1 or 2. * - * @param mixed $value * @return static self reference */ public function ownerId(mixed $value): VariantQuery @@ -284,7 +268,6 @@ public function ownerId(mixed $value): VariantQuery * | `[1, 2]` | for product of a type with an ID of 1 or 2. * | `['not', 1, 2]` | for product of a type not with an ID of 1 or 2. * - * @param mixed $value * @return static self reference */ public function typeId(mixed $value): VariantQuery @@ -702,7 +685,6 @@ protected function beforePrepare(): bool /** * Normalizes the primaryOwnerId param to an array of IDs or null * - * @param mixed $value * @return int[]|null * @throws InvalidArgumentException */ diff --git a/src/elements/traits/OrderElementTrait.php b/src/elements/traits/OrderElementTrait.php index bdc9aed441..f2cac873a2 100644 --- a/src/elements/traits/OrderElementTrait.php +++ b/src/elements/traits/OrderElementTrait.php @@ -16,6 +16,7 @@ use craft\commerce\elements\conditions\orders\OrderStatusConditionRule; use craft\commerce\elements\db\OrderQuery; use craft\commerce\exports\Expanded; +use craft\commerce\models\OrderStatus; use craft\commerce\Plugin; use craft\elements\actions\Delete; use craft\elements\actions\Restore; @@ -390,7 +391,7 @@ protected static function defineActions(string $source): array $site = Cp::requestedSite(); $store = $site->getStore(); // Remove nested "all" prefix if it exists at the start of the string - $source = strpos($source, '*/') === 0 ? substr($source, 2) : $source; + $source = str_starts_with($source, '*/') ? substr($source, 2) : $source; $elementService = Craft::$app->getElements(); @@ -535,42 +536,14 @@ public static function prepElementQueryForTableAttribute(ElementQueryInterface $ { /** @var OrderQuery $elementQuery */ - switch ($attribute) { - case 'totals': - case 'total': - case 'totalPrice': - case 'totalDiscount': - case 'totalShippingCost': - case 'totalTax': - case 'totalIncludedTax': - $elementQuery->withAdjustments(); - break; - case 'totalPaid': - case 'paidStatus': - $elementQuery->withTransactions(); - break; - case 'shippingFullName': - case 'shippingFirstName': - case 'shippingLastName': - case 'billingFullName': - case 'billingFirstName': - case 'billingLastName': - case 'shippingOrganizationName': - case 'billingOrganizationName': - case 'shippingMethodName': - $elementQuery->withAddresses(); - break; - case 'email': - case 'customer': - $elementQuery->withCustomer(); - break; - case 'itemTotal': - case 'itemSubtotal': - $elementQuery->withLineItems(); - break; - default: - parent::prepElementQueryForTableAttribute($elementQuery, $attribute); - } + match ($attribute) { + 'totals', 'total', 'totalPrice', 'totalDiscount', 'totalShippingCost', 'totalTax', 'totalIncludedTax' => $elementQuery->withAdjustments(), + 'totalPaid', 'paidStatus' => $elementQuery->withTransactions(), + 'shippingFullName', 'shippingFirstName', 'shippingLastName', 'billingFullName', 'billingFirstName', 'billingLastName', 'shippingOrganizationName', 'billingOrganizationName', 'shippingMethodName' => $elementQuery->withAddresses(), + 'email', 'customer' => $elementQuery->withCustomer(), + 'itemTotal', 'itemSubtotal' => $elementQuery->withLineItems(), + default => parent::prepElementQueryForTableAttribute($elementQuery, $attribute), + }; } /** @@ -703,4 +676,162 @@ public static function modifyCustomSource(array $config): array return $config; } + + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + /** @var OrderStatus $status */ + $status = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()->first(); + $site = Craft::$app->getSites()->getCurrentSite(); + $number = Plugin::getInstance()->getCarts()->generateCartNumber(); + + return array_merge(parent::defineCardAttributes(), [ + 'shortNumber' => [ + 'label' => Craft::t('commerce', 'Short Number'), + 'placeholder' => substr($number, 0, 7), + ], + 'number' => [ + 'label' => Craft::t('commerce', 'Number'), + 'placeholder' => $number, + ], + 'id' => [ + 'label' => Craft::t('commerce', 'ID'), + 'placeholder' => '12345', + ], + 'orderStatus' => [ + 'label' => Craft::t('commerce', 'Status'), + 'placeholder' => $status->getLabelHtml(), + ], + 'totalQty' => [ + 'label' => Craft::t('commerce', 'Total Qty'), + 'placeholder' => '10', + ], + 'total' => [ + 'label' => Craft::t('commerce', 'Total'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'totalPrice' => [ + 'label' => Craft::t('commerce', 'Total Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'totalPaid' => [ + 'label' => Craft::t('commerce', 'Total Paid'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'totalDiscount' => [ + 'label' => Craft::t('commerce', 'Total Discount'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(12.99), + ], + 'totalShippingCost' => [ + 'label' => Craft::t('commerce', 'Total Shipping'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(9.99), + ], + 'totalTax' => [ + 'label' => Craft::t('commerce', 'Total Tax'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(19.99), + ], + 'totalIncludedTax' => [ + 'label' => Craft::t('commerce', 'Total Included Tax'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(19.99), + ], + 'dateOrdered' => [ + 'label' => Craft::t('commerce', 'Date Ordered'), + 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), + ], + 'datePaid' => [ + 'label' => Craft::t('commerce', 'Date Paid'), + 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), + ], + 'dateUpdated' => [ + 'label' => Craft::t('commerce', 'Date Updated'), + 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), + ], + 'email' => [ + 'label' => Craft::t('commerce', 'Email'), + 'placeholder' => 'user@example.com', + ], + 'customer' => [ + 'label' => Craft::t('commerce', 'Customer'), + 'placeholder' => Craft::t('commerce', 'Customer'), + ], + 'shippingFullName' => [ + 'label' => Craft::t('commerce', 'Shipping Full Name'), + 'placeholder' => Craft::t('commerce', 'Shipping Full Name'), + ], + 'shippingFirstName' => [ + 'label' => Craft::t('commerce', 'Shipping First Name'), + 'placeholder' => Craft::t('commerce', 'Shipping First Name'), + ], + 'shippingLastName' => [ + 'label' => Craft::t('commerce', 'Shipping Last Name'), + 'placeholder' => Craft::t('commerce', 'Shipping Last Name'), + ], + 'billingFullName' => [ + 'label' => Craft::t('commerce', 'Billing Full Name'), + 'placeholder' => Craft::t('commerce', 'Billing Full Name'), + ], + 'billingFirstName' => [ + 'label' => Craft::t('commerce', 'Billing First Name'), + 'placeholder' => Craft::t('commerce', 'Billing First Name'), + ], + 'billingLastName' => [ + 'label' => Craft::t('commerce', 'Billing Last Name'), + 'placeholder' => Craft::t('commerce', 'Billing Last Name'), + ], + 'shippingOrganizationName' => [ + 'label' => Craft::t('commerce', 'Shipping Business Name'), + 'placeholder' => Craft::t('commerce', 'Shipping Business Name'), + ], + 'billingOrganizationName' => [ + 'label' => Craft::t('commerce', 'Billing Business Name'), + 'placeholder' => Craft::t('commerce', 'Billing Business Name'), + ], + 'shippingMethodName' => [ + 'label' => Craft::t('commerce', 'Shipping Method'), + 'placeholder' => Craft::t('commerce', 'Shipping Method'), + ], + 'gatewayName' => [ + 'label' => Craft::t('commerce', 'Gateway'), + 'placeholder' => Craft::t('commerce', 'Gateway'), + ], + 'paidStatus' => [ + 'label' => Craft::t('commerce', 'Paid Status'), + 'placeholder' => Cp::statusLabelHtml(['color' => 'green', 'label' => Craft::t('commerce', 'Paid')]), + ], + 'couponCode' => [ + 'label' => Craft::t('commerce', 'Coupon Code'), + 'placeholder' => 'SAVE10', + ], + 'itemTotal' => [ + 'label' => Craft::t('commerce', 'Item Total'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(99.99), + ], + 'itemSubtotal' => [ + 'label' => Craft::t('commerce', 'Item Subtotal'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(89.99), + ], + 'orderSite' => [ + 'label' => Craft::t('commerce', 'Order Site'), + 'placeholder' => $site->name, + ], + 'reference' => [ + 'label' => Craft::t('commerce', 'Reference'), + 'placeholder' => 'ORD-XXXXX', + ], + ]); + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'reference', + 'orderStatus', + 'totalPrice', + ]); + } } diff --git a/src/elements/traits/OrderNoticesTrait.php b/src/elements/traits/OrderNoticesTrait.php index 42a64dc5ea..4e95d11d66 100644 --- a/src/elements/traits/OrderNoticesTrait.php +++ b/src/elements/traits/OrderNoticesTrait.php @@ -50,9 +50,7 @@ public function getNotices(?string $type = null, ?string $attribute = null): arr // Filter by both if ($type !== null && $attribute !== null) { - return ArrayHelper::where($this->_notices, function(OrderNotice $notice) use ($attribute, $type) { - return $notice->attribute == $attribute && $notice->type == $type; - }, true, true, true); + return ArrayHelper::where($this->_notices, fn(OrderNotice $notice) => $notice->attribute == $attribute && $notice->type == $type, true, true, true); } return []; @@ -105,17 +103,11 @@ public function clearNotices(?string $type = null, ?string $attribute = null): v if ($type === null && $attribute === null) { $this->_notices = []; } elseif ($type !== null && $attribute === null) { - $this->_notices = ArrayHelper::where($this->_notices, function(OrderNotice $notice) use ($type) { - return $notice->type != $type; - }, true, true, true); + $this->_notices = ArrayHelper::where($this->_notices, fn(OrderNotice $notice) => $notice->type != $type, true, true, true); } elseif ($type === null && $attribute !== null) { - $this->_notices = ArrayHelper::where($this->_notices, function(OrderNotice $notice) use ($attribute) { - return $notice->attribute != $attribute; - }, true, true, true); + $this->_notices = ArrayHelper::where($this->_notices, fn(OrderNotice $notice) => $notice->attribute != $attribute, true, true, true); } elseif ($type !== null && $attribute !== null) { - $this->_notices = ArrayHelper::where($this->_notices, function(OrderNotice $notice) use ($type, $attribute) { - return $notice->type == $type && $notice->attribute == $attribute; - }, false, true, true); + $this->_notices = ArrayHelper::where($this->_notices, fn(OrderNotice $notice) => $notice->type == $type && $notice->attribute == $attribute, false, true, true); } } diff --git a/src/errors/LineItemNotFoundException.php b/src/errors/LineItemNotFoundException.php new file mode 100644 index 0000000000..b37faf8c63 --- /dev/null +++ b/src/errors/LineItemNotFoundException.php @@ -0,0 +1,27 @@ + + * @since 4.9 + */ +class LineItemNotFoundException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName(): string + { + return 'Line Item not found'; + } +} diff --git a/src/errors/OrderAdjustmentNotFoundException.php b/src/errors/OrderAdjustmentNotFoundException.php new file mode 100644 index 0000000000..b43f66c53b --- /dev/null +++ b/src/errors/OrderAdjustmentNotFoundException.php @@ -0,0 +1,27 @@ + + * @since 4.9 + */ +class OrderAdjustmentNotFoundException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName(): string + { + return 'Line Item not found'; + } +} diff --git a/src/events/MailEvent.php b/src/events/MailEvent.php index 3ff8ff5834..c60810389d 100644 --- a/src/events/MailEvent.php +++ b/src/events/MailEvent.php @@ -44,5 +44,5 @@ class MailEvent extends CancelableEvent /** * @var array Order data at the time the email sends. */ - public ?array $orderData; + public ?array $orderData = null; } diff --git a/src/events/PdfRenderEvent.php b/src/events/PdfRenderEvent.php index b7b12bff9f..2b46d5364b 100644 --- a/src/events/PdfRenderEvent.php +++ b/src/events/PdfRenderEvent.php @@ -46,6 +46,7 @@ class PdfRenderEvent extends Event /** * @var Pdf|null The configured PDF model used to render the PDF + * @since 5.0.12 */ public ?Pdf $sourcePdf = null; } diff --git a/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php b/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php index 912e268da1..455c920f7c 100644 --- a/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php +++ b/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php @@ -12,6 +12,7 @@ use craft\commerce\base\Purchasable; use craft\commerce\helpers\Purchasable as PurchasableHelper; use craft\fieldlayoutelements\BaseNativeField; +use craft\helpers\Cp; use yii\base\InvalidArgumentException; /** @@ -32,6 +33,11 @@ class PurchasableAvailableForPurchaseField extends BaseNativeField */ public string $attribute = 'availableForPurchase'; + /** + * @var bool Whether the field should be checked by default when creating a new purchasable. + */ + public bool $defaultAvailableForPurchase = false; + /** * @inheritdoc */ @@ -50,11 +56,23 @@ public function inputHtml(ElementInterface $element = null, bool $static = false throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); } - return PurchasableHelper::availableForPurchaseInputHtml($element->availableForPurchase, [ + return PurchasableHelper::availableForPurchaseInputHtml($element->getIsFresh() ? $this->defaultAvailableForPurchase : $element->availableForPurchase, [ 'disabled' => $static, ]); } + public function settingsHtml(): string + { + return parent::settingsHtml() . Cp::lightswitchHtml( + [ + 'id' => 'defaultAvailableForPurchase', + 'name' => 'defaultAvailableForPurchase', + 'label' => Craft::t('app', 'Default Value'), + 'on' => $this->defaultAvailableForPurchase, + ] + ); + } + /** * @inheritdoc */ diff --git a/src/fieldlayoutelements/PurchasablePriceField.php b/src/fieldlayoutelements/PurchasablePriceField.php index 1014a66d65..fde9f48c78 100755 --- a/src/fieldlayoutelements/PurchasablePriceField.php +++ b/src/fieldlayoutelements/PurchasablePriceField.php @@ -52,6 +52,14 @@ class PurchasablePriceField extends BaseNativeField */ public bool $required = true; + /** + * @inheritdoc + */ + protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string + { + return Craft::t('commerce', 'Price'); + } + /** * @inheritdoc */ @@ -90,7 +98,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false $purchasableConditionRule = Craft::$app->getConditions()->createConditionRule([ 'class' => CatalogPricingPurchasableConditionRule::class, - 'elementIds' => [get_class($element) => [$element->id]], + 'elementIds' => [$element::class => [$element->id]], ]); $catalogPricingCondition->addConditionRule($purchasableConditionRule); $conditionBuilderConfig = Json::encode($catalogPricingCondition->getConfig()); diff --git a/src/fieldlayoutelements/PurchasablePromotableField.php b/src/fieldlayoutelements/PurchasablePromotableField.php index 21074b7826..f3789bb2f8 100644 --- a/src/fieldlayoutelements/PurchasablePromotableField.php +++ b/src/fieldlayoutelements/PurchasablePromotableField.php @@ -32,6 +32,11 @@ class PurchasablePromotableField extends BaseNativeField */ public string $attribute = 'promotable'; + /** + * @var bool Whether the field should be checked by default when creating a new purchasable. + */ + public bool $defaultPromotable = false; + /** * @inheritdoc */ @@ -54,11 +59,23 @@ public function inputHtml(ElementInterface $element = null, bool $static = false 'id' => 'promotable', 'name' => 'promotable', 'small' => true, - 'on' => $element->promotable, + 'on' => $element->getIsFresh() ? $this->defaultPromotable : $element->promotable, 'disabled' => $static, ]); } + public function settingsHtml(): string + { + return parent::settingsHtml() . Cp::lightswitchHtml( + [ + 'id' => 'defaultPromotable', + 'name' => 'defaultPromotable', + 'label' => Craft::t('app', 'Default Value'), + 'on' => $this->defaultPromotable, + ] + ); + } + /** * @inheritdoc */ diff --git a/src/fieldlayoutelements/PurchasableStockField.php b/src/fieldlayoutelements/PurchasableStockField.php index 8f51aec5bc..0fd969e8c7 100644 --- a/src/fieldlayoutelements/PurchasableStockField.php +++ b/src/fieldlayoutelements/PurchasableStockField.php @@ -37,6 +37,16 @@ class PurchasableStockField extends BaseNativeField */ public string $attribute = 'stock'; + /** + * @var bool Whether inventory should be tracked by default when creating a new purchasable. + */ + public bool $defaultInventoryTracked = false; + + /** + * @var bool Whether out of stock purchases should be allowed by default when creating a new purchasable. + */ + public bool $defaultAllowOutOfStockPurchases = false; + /** * @inheritdoc */ @@ -119,80 +129,79 @@ public function inputHtml(ElementInterface $element = null, bool $static = false $inventoryLevelTableRows .= Html::beginTag('tr') . Html::beginTag('td') . - $inventoryLevel->getInventoryLocation()->name . + $inventoryLevel->getInventoryLocation()->name . Html::endTag('td') . Html::beginTag('td') . - Html::beginTag('div', ['class' => 'flex']) . - Html::tag('div', (string)$inventoryLevel->availableTotal, [ - 'id' => $updatedValueId, - ]) . - (!$static ? Html::tag('div',Html::button(Craft::t('commerce', ''), - [ - 'class' => 'btn menubtn action-btn', - 'id' => $editUpdateQuantityInventoryItemId, - ])) : '') . - Html::endTag('div') . + Html::beginTag('div', ['class' => 'flex']) . + Html::tag('div', (string)$inventoryLevel->availableTotal, [ + 'id' => $updatedValueId, + ]) . + (!$static ? Html::tag('div', Html::button(Craft::t('commerce', ''), + [ + 'class' => 'btn menubtn action-btn', + 'id' => $editUpdateQuantityInventoryItemId, + ])) : '') . + Html::endTag('div') . Html::endTag('td') . (!$static ? Html::beginTag('td') . (Craft::$app->getUser()->checkPermission('commerce-manageInventoryStockLevels') ? - Html::a( - Craft::t('commerce', 'Manage'), - UrlHelper::cpUrl('commerce/inventory/levels/' . $inventoryLevel->getInventoryLocation()->handle, [ - 'inventoryItemId' => $inventoryLevel->getInventoryItem()->id, - ]), - [ - 'target' => '_blank', - 'class' => 'btn small', - 'id' => $editUpdateQuantityInventoryItemId, - 'aria-label' => Craft::t('app', 'Open in a new tab'), - 'data-icon' => 'external', - ] - ) : '') : '') . + Html::a( + Craft::t('commerce', 'Manage'), + UrlHelper::cpUrl('commerce/inventory/levels/' . $inventoryLevel->getInventoryLocation()->handle, [ + 'inventoryItemId' => $inventoryLevel->getInventoryItem()->id, + ]), + [ + 'target' => '_blank', + 'class' => 'btn small', + 'id' => $editUpdateQuantityInventoryItemId, + 'aria-label' => Craft::t('app', 'Open in a new tab'), + 'data-icon' => 'external', + ] + ) : '') : '') . Html::endTag('td') . Html::endTag('tr'); } $inventoryLevelsTable = Html::beginTag('table', ['class' => 'data fullwidth', 'style' => 'margin-top:5px;']) . Html::beginTag('thead') . - Html::beginTag('tr') . - Html::beginTag('th') . - Craft::t('commerce', 'Location') . - Html::endTag('th') . - Html::beginTag('th') . - Craft::t('commerce', 'Available') . - Html::endTag('th') . - + Html::beginTag('tr') . + Html::beginTag('th') . + Craft::t('commerce', 'Location') . + Html::endTag('th') . + Html::beginTag('th') . + Craft::t('commerce', 'Available') . + Html::endTag('th') . - (!$static ? Html::beginTag('th') . - Craft::t('commerce', 'Manage') . - Html::endTag('th') : '') . + (!$static ? Html::beginTag('th') . + Craft::t('commerce', 'Manage') . + Html::endTag('th') : '') . - Html::endTag('tr') . + Html::endTag('tr') . Html::endTag('thead') . Html::beginTag('tbody') . - $inventoryLevelTableRows . - Html::beginTag('tr') . - Html::beginTag('td', ['colspan' => '2']) . - $availableStockLabel . - Html::endTag('td') . + $inventoryLevelTableRows . + Html::beginTag('tr') . + Html::beginTag('td', ['colspan' => '2']) . + $availableStockLabel . + Html::endTag('td') . - (!$static ? Html::beginTag('td') . - Html::a( - Craft::t('commerce', 'Edit'), - '#', - [ - 'class' => 'btn small', - 'id' => $editInventoryItemId, - 'aria-label' => Craft::t('app', 'Edit Inventory Item'), - 'data-icon' => 'edit', - ] - ) . - Html::endTag('td') : '') . + (!$static ? Html::beginTag('td') . + Html::a( + Craft::t('commerce', 'Edit'), + '#', + [ + 'class' => 'btn small', + 'id' => $editInventoryItemId, + 'aria-label' => Craft::t('app', 'Edit Inventory Item'), + 'data-icon' => 'edit', + ] + ) . + Html::endTag('td') : '') . - Html::endTag('tr') . + Html::endTag('tr') . Html::endTag('tbody') . Html::endTag('table'); @@ -201,7 +210,7 @@ public function inputHtml(ElementInterface $element = null, bool $static = false 'id' => 'store-inventory-item-tracked', 'name' => 'inventoryTracked', 'small' => true, - 'on' => $element->inventoryTracked, + 'on' => $element->getIsFresh() ? $this->defaultInventoryTracked : $element->inventoryTracked, 'toggle' => $inventoryItemTrackedId, 'disabled' => $static, ]; @@ -211,20 +220,38 @@ public function inputHtml(ElementInterface $element = null, bool $static = false 'id' => 'store-backorder-allowed', 'name' => 'allowOutOfStockPurchases', 'small' => true, - 'on' => $element->getIsOutOfStockPurchasingAllowed(), + 'on' => $element->getIsFresh() ? $this->defaultAllowOutOfStockPurchases : $element->getIsOutOfStockPurchasingAllowed(), 'disabled' => $static, ]; return Html::beginTag('div') . - Cp::lightswitchHtml($storeInventoryTrackedLightswitchConfig) . - Html::beginTag('div', ['id' => $inventoryItemTrackedId, 'class' => 'hidden']) . - $inventoryLevelsTable . - Cp::lightswitchFieldHtml($storeAllowOutOfStockPurchasesLightswitchConfig) . - Html::endTag('div') . + Cp::lightswitchHtml($storeInventoryTrackedLightswitchConfig) . + Html::beginTag('div', ['id' => $inventoryItemTrackedId, 'class' => 'hidden']) . + $inventoryLevelsTable . + Cp::lightswitchFieldHtml($storeAllowOutOfStockPurchasesLightswitchConfig) . + Html::endTag('div') . Html::endTag('div'); } + public function settingsHtml(): string + { + $lightSwitches = Cp::lightswitchHtml([ + 'id' => 'defaultInventoryTracked', + 'name' => 'defaultInventoryTracked', + 'label' => Craft::t('commerce', 'Track Inventory'), + 'on' => $this->defaultInventoryTracked, + ]) . + Cp::lightswitchHtml([ + 'id' => 'defaultAllowOutOfStockPurchases', + 'name' => 'defaultAllowOutOfStockPurchases', + 'label' => Craft::t('commerce', 'Allow out of stock purchases'), + 'on' => $this->defaultAllowOutOfStockPurchases, + ]); + + return parent::settingsHtml() . Cp::fieldHtml($lightSwitches, ['label' => Craft::t('app', 'Default Value')]); + } + /** * @inheritdoc */ diff --git a/src/fieldlayoutelements/VariantsField.php b/src/fieldlayoutelements/VariantsField.php index a2a9bf4ffd..ceef5f02d6 100644 --- a/src/fieldlayoutelements/VariantsField.php +++ b/src/fieldlayoutelements/VariantsField.php @@ -63,6 +63,7 @@ protected function inputHtml(ElementInterface $element = null, bool $static = fa return $element->getVariantManager()->getIndexHtml($element, [ 'canCreate' => !$static, + 'canPaste' => !$static, 'minElements' => 0, 'maxElements' => $maxVariants ?? null, 'allowedViewModes' => [ElementIndexViewMode::Cards, ElementIndexViewMode::Table], diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 83fce974fb..dd50ca89a0 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -9,7 +9,9 @@ use Craft; use craft\commerce\elements\Product as ProductElement; +use craft\commerce\gql\arguments\elements\Product as ProductArguments; use craft\commerce\gql\types\generators\ProductType; +use craft\commerce\helpers\Gql; use craft\gql\GqlEntityRegistry; use craft\gql\interfaces\Element; use GraphQL\Type\Definition\InterfaceType; @@ -44,9 +46,7 @@ public static function getType($fields = null): Type 'name' => static::getName(), 'fields' => self::class . '::getFieldDefinitions', 'description' => 'This is the interface implemented by all products.', - 'resolveType' => function(ProductElement $value) { - return $value->getGqlTypeName(); - }, + 'resolveType' => fn(ProductElement $value) => $value->getGqlTypeName(), ])); ProductType::generateTypes(); @@ -128,6 +128,13 @@ public static function getFieldDefinitions(): array 'type' => Type::listOf(Variant::getType()), 'description' => 'The product’s variants.', ], + 'localized' => [ + 'name' => 'localized', + 'args' => ProductArguments::getArguments(), + 'type' => Type::nonNull(Type::listOf(Type::nonNull(static::getType()))), + 'description' => 'The same element in other locales.', + 'complexity' => Gql::eagerLoadComplexity(), + ], ]), self::getName()); } } diff --git a/src/gql/interfaces/elements/Variant.php b/src/gql/interfaces/elements/Variant.php index d75ce0a8d7..ac95975b1b 100644 --- a/src/gql/interfaces/elements/Variant.php +++ b/src/gql/interfaces/elements/Variant.php @@ -45,9 +45,7 @@ public static function getType($fields = null): Type 'name' => static::getName(), 'fields' => self::class . '::getFieldDefinitions', 'description' => 'This is the interface implemented by all variants.', - 'resolveType' => function(VariantElement $value) { - return $value->getGqlTypeName(); - }, + 'resolveType' => fn(VariantElement $value) => $value->getGqlTypeName(), ])); VariantType::generateTypes(); diff --git a/src/gql/types/generators/ProductType.php b/src/gql/types/generators/ProductType.php index efd208a899..dea91814c1 100644 --- a/src/gql/types/generators/ProductType.php +++ b/src/gql/types/generators/ProductType.php @@ -56,9 +56,7 @@ public static function generateTypes(mixed $context = null): array // Generate a type for each product type $gqlTypes[$typeName] = GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new ProductTypeElement([ 'name' => $typeName, - 'fields' => function() use ($productTypeFields) { - return $productTypeFields; - }, + 'fields' => fn() => $productTypeFields, ])); } diff --git a/src/gql/types/generators/VariantType.php b/src/gql/types/generators/VariantType.php index f63a1f8e23..971c051c00 100644 --- a/src/gql/types/generators/VariantType.php +++ b/src/gql/types/generators/VariantType.php @@ -57,9 +57,7 @@ public static function generateTypes(mixed $context = null): array // Generate a type for each product type $gqlTypes[$typeName] = GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new Variant([ 'name' => $typeName, - 'fields' => function() use ($fields) { - return $fields; - }, + 'fields' => fn() => $fields, ])); } diff --git a/src/gql/types/input/Product.php b/src/gql/types/input/Product.php index 3f94dfe46d..e7a416b095 100644 --- a/src/gql/types/input/Product.php +++ b/src/gql/types/input/Product.php @@ -28,9 +28,7 @@ public static function getType(): mixed return GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new InputObjectType([ 'name' => $typeName, - 'fields' => function() { - return ProductArguments::getArguments(); - }, + 'fields' => fn() => ProductArguments::getArguments(), ])); } } diff --git a/src/gql/types/input/Variant.php b/src/gql/types/input/Variant.php index c3f01ebfc6..6de000cc79 100644 --- a/src/gql/types/input/Variant.php +++ b/src/gql/types/input/Variant.php @@ -28,9 +28,7 @@ public static function getType(): mixed return GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new InputObjectType([ 'name' => $typeName, - 'fields' => function() { - return VariantArguments::getArguments(); - }, + 'fields' => fn() => VariantArguments::getArguments(), ])); } } diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php index 7c1dc56a72..a96fe9965c 100644 --- a/src/helpers/Cp.php +++ b/src/helpers/Cp.php @@ -26,7 +26,7 @@ class Cp */ public static function inventoryLocationFieldHtml(array $config): string { - $config['id'] = $config['id'] ?? 'inventorylocationselect' . mt_rand(); + $config['id'] ??= 'inventorylocationselect' . mt_rand(); return CraftCp::fieldHtml('template:commerce/_includes/forms/inventoryLocationSelect.twig', $config); } } diff --git a/src/helpers/Currency.php b/src/helpers/Currency.php index d5ce390d84..9e6c044647 100644 --- a/src/helpers/Currency.php +++ b/src/helpers/Currency.php @@ -68,7 +68,6 @@ public static function defaultDecimals(): int * Formats and optionally converts a currency amount into the supplied valid payment currency as per the rate setup in payment currencies. * * @param $amount - * @param mixed $currency * @param bool $convert * @param bool $format * @param bool $stripZeros @@ -123,7 +122,6 @@ public static function formatAsCurrency($amount, mixed $currency = null, bool $c } /** - * @param mixed $value * @param array $config * @return string * @throws InvalidConfigException diff --git a/src/helpers/DebugPanel.php b/src/helpers/DebugPanel.php index d96a8ff1a1..57a1d42a12 100644 --- a/src/helpers/DebugPanel.php +++ b/src/helpers/DebugPanel.php @@ -32,7 +32,7 @@ class DebugPanel public static function prependOrAppendModelTab(object $model, ?string $name = null, bool $prepend = false): void { if (!$name) { - $classSegments = explode('\\', get_class($model)); + $classSegments = explode('\\', $model::class); $name = array_pop($classSegments); if (property_exists($model, 'id')) { @@ -67,7 +67,6 @@ public static function prependOrAppendModelTab(object $model, ?string $name = nu /** * @param string $attr - * @param mixed $value * @param string|null $label * @return string */ diff --git a/src/helpers/ProjectConfigData.php b/src/helpers/ProjectConfigData.php index 5dbb174256..d5d4e8eb6f 100755 --- a/src/helpers/ProjectConfigData.php +++ b/src/helpers/ProjectConfigData.php @@ -21,7 +21,6 @@ use craft\commerce\services\ProductTypes; use craft\commerce\services\Stores; use craft\db\Query; -use craft\helpers\Json; /** * Class ProjectConfigData @@ -116,29 +115,11 @@ private static function _getProjectConfigKey(string $key): string */ private static function _rebuildGatewayProjectConfig(): array { - $gatewayData = (new Query()) - ->select(['*']) - ->from([Table::GATEWAYS]) - ->where(['isArchived' => false]) - ->all(); - - $configData = []; - - foreach ($gatewayData as $gatewayRow) { - $settings = Json::decodeIfJson($gatewayRow['settings']); - $configData[$gatewayRow['uid']] = [ - 'name' => $gatewayRow['name'], - 'handle' => $gatewayRow['handle'], - 'type' => $gatewayRow['type'], - 'settings' => $settings, - 'sortOrder' => (int)$gatewayRow['sortOrder'], - 'paymentType' => $gatewayRow['paymentType'], - 'isFrontendEnabled' => (bool)$gatewayRow['isFrontendEnabled'], - ]; + $data = []; + foreach (Plugin::getInstance()->getGateways()->getAllGateways() as $gateway) { + $data[$gateway->uid] = $gateway->getConfig(); } - - - return $configData; + return $data; } /** diff --git a/src/helpers/Purchasable.php b/src/helpers/Purchasable.php index fa1351c2d1..a1aa26f6c4 100644 --- a/src/helpers/Purchasable.php +++ b/src/helpers/Purchasable.php @@ -55,7 +55,7 @@ public static function isTempSku(string $sku): bool */ public static function catalogPricingRulesTableByPurchasableId(int $purchasableId, int $storeId, ?Collection $catalogPricing = null): string { - $catalogPricing = $catalogPricing ?? Plugin::getInstance()->getCatalogPricing()->getCatalogPricesByPurchasableId($purchasableId, $storeId); + $catalogPricing ??= Plugin::getInstance()->getCatalogPricing()->getCatalogPricesByPurchasableId($purchasableId, $storeId); $catalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()->getAllCatalogPricingRulesByPurchasableId($purchasableId, $storeId); if ($catalogPricingRules->isEmpty()) { diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 374f953d8d..17783f1a28 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -297,6 +297,7 @@ public function createTables(): void 'settings' => $this->text(), 'paymentType' => $this->enum('paymentType', ['authorize', 'purchase'])->notNull()->defaultValue('purchase'), 'isFrontendEnabled' => $this->string(500)->notNull()->defaultValue('1'), + 'orderCondition' => $this->text(), 'isArchived' => $this->boolean()->notNull()->defaultValue(false), 'dateArchived' => $this->dateTime(), 'sortOrder' => $this->integer(), @@ -789,6 +790,7 @@ public function createTables(): void 'name' => $this->string()->notNull(), 'handle' => $this->string()->notNull(), 'orderCondition' => $this->text(), + 'customerCondition' => $this->text(), 'enabled' => $this->boolean()->notNull()->defaultValue(true), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), @@ -819,6 +821,7 @@ public function createTables(): void 'enabled' => $this->boolean()->notNull()->defaultValue(true), 'orderConditionFormula' => $this->text(), 'orderCondition' => $this->text(), + 'customerCondition' => $this->text(), 'baseRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), 'perItemRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), 'weightRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), @@ -1425,6 +1428,7 @@ private function _defaultGateways(): void 'name' => 'Dummy', 'handle' => 'dummy', 'isFrontendEnabled' => true, + 'orderCondition' => [], 'isArchived' => false, ]; $gateway = new Dummy($data); diff --git a/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php b/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php index b563bb2765..a9d6b946aa 100644 --- a/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php +++ b/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php @@ -98,8 +98,6 @@ public function safeUp(): bool /** * @param OrderValuesAttributeConditionRule|OrderCurrencyValuesAttributeConditionRule $rule * @param ShippingRuleOrderCondition $orderCondition - * @param mixed $min - * @param mixed $max * @param bool $adjustValues * @return ShippingRuleOrderCondition */ diff --git a/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php b/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php index 37f7622df3..86a6adf15b 100644 --- a/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php +++ b/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php @@ -28,18 +28,14 @@ public function safeUp(): bool ->from([$discountPurchasablesTables]) ->collect(); - $purchasableIdsByDiscountId = $purchasableIdsByDiscountId->groupBy('discountId')->map(function($row) { - return array_column($row->toArray(), 'purchasableId'); - }); + $purchasableIdsByDiscountId = $purchasableIdsByDiscountId->groupBy('discountId')->map(fn($row) => array_column($row->toArray(), 'purchasableId')); $categoryIdsByDiscountId = (new Query()) ->select(['discountId', 'categoryId']) ->from([$discountCategoriesTable]) ->collect(); - $categoryIdsByDiscountId = $categoryIdsByDiscountId->groupBy('discountId')->map(function($row) { - return array_column($row->toArray(), 'categoryId'); - }); + $categoryIdsByDiscountId = $categoryIdsByDiscountId->groupBy('discountId')->map(fn($row) => array_column($row->toArray(), 'categoryId')); foreach ($purchasableIdsByDiscountId as $discountId => $purchasableIds) { $this->update($discountsTable, ['purchasableIds' => Json::encode($purchasableIds)], ['id' => $discountId]); diff --git a/src/migrations/m240529_095819_remove_commerce_user_field.php b/src/migrations/m240529_095819_remove_commerce_user_field.php index 886554c053..7d08fe584b 100644 --- a/src/migrations/m240529_095819_remove_commerce_user_field.php +++ b/src/migrations/m240529_095819_remove_commerce_user_field.php @@ -27,7 +27,7 @@ public function safeUp(): bool $newFields = []; foreach ($tab->elements as $element) { - if (get_class($element) !== $fieldClassName) { + if ($element::class !== $fieldClassName) { $newFields[] = $element; } } diff --git a/src/migrations/m240923_132625_remove_orphaned_variants_sites.php b/src/migrations/m240923_132625_remove_orphaned_variants_sites.php index ce7bac534d..d56046062f 100644 --- a/src/migrations/m240923_132625_remove_orphaned_variants_sites.php +++ b/src/migrations/m240923_132625_remove_orphaned_variants_sites.php @@ -23,9 +23,7 @@ public function safeUp(): bool ->collect(); // Group them by product ID - $siteIdsByProductId = $allProductsSites->groupBy('elementId')->map(function($row) { - return collect($row)->pluck('siteId')->toArray(); - } + $siteIdsByProductId = $allProductsSites->groupBy('elementId')->map(fn($row) => collect($row)->pluck('siteId')->toArray() ); // Find all existing combinations of variant and site IDs @@ -36,9 +34,7 @@ public function safeUp(): bool ->collect(); // Find all variants that are not associated with any of their product's sites - $orphanedVariantsSites = array_values($allVariantsSites->filter(function($row) use ($siteIdsByProductId) { - return !in_array($row['siteId'], $siteIdsByProductId[$row['primaryOwnerId']]); - })->map(fn($row) => $row['id'])->toArray()); + $orphanedVariantsSites = array_values($allVariantsSites->filter(fn($row) => !in_array($row['siteId'], $siteIdsByProductId[$row['primaryOwnerId']]))->map(fn($row) => $row['id'])->toArray()); if (empty($orphanedVariantsSites)) { return true; diff --git a/src/migrations/m250301_120000_add_gateway_order_condition.php b/src/migrations/m250301_120000_add_gateway_order_condition.php new file mode 100644 index 0000000000..315cc920f3 --- /dev/null +++ b/src/migrations/m250301_120000_add_gateway_order_condition.php @@ -0,0 +1,50 @@ +addColumn(Table::GATEWAYS, 'orderCondition', $this->text()); + + $gateways = (new Query()) + ->select(['id']) + ->from(Table::GATEWAYS) + ->all(); + + foreach ($gateways as $gateway) { + $orderCondition = [ + 'class' => 'craft\\commerce\\elements\\conditions\\orders\\GatewayOrderCondition', + 'conditionRules' => [], + ]; + + $this->update(Table::GATEWAYS, + ['orderCondition' => json_encode($orderCondition)], + ['id' => $gateway['id']] + ); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m250301_120000_add_gateway_order_condition cannot be reverted.\n"; + return false; + } +} diff --git a/src/migrations/m250401_091214_add_shipping_method_customer_condition.php b/src/migrations/m250401_091214_add_shipping_method_customer_condition.php new file mode 100644 index 0000000000..a28354331b --- /dev/null +++ b/src/migrations/m250401_091214_add_shipping_method_customer_condition.php @@ -0,0 +1,31 @@ +addColumn(Table::SHIPPINGMETHODS, 'customerCondition', $this->text()); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m250401_091214_add_shipping_method_customer_condition cannot be reverted.\n"; + return false; + } +} diff --git a/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php b/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php new file mode 100644 index 0000000000..cbd37bc337 --- /dev/null +++ b/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php @@ -0,0 +1,31 @@ +addColumn(Table::SHIPPINGRULES, 'customerCondition', $this->text()); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m250403_134328_add_shipping_rule_customer_condition cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/CatalogPricingRule.php b/src/models/CatalogPricingRule.php index d7011e0f18..93670b2502 100644 --- a/src/models/CatalogPricingRule.php +++ b/src/models/CatalogPricingRule.php @@ -166,6 +166,8 @@ protected function defineRules(): array 'applyAmount', 'applyPriceType', 'customerCondition', + 'dateUpdated', + 'dateCreated', 'dateFrom', 'dateTo', 'description', diff --git a/src/models/Email.php b/src/models/Email.php index edee18c933..7fa88f5fd4 100644 --- a/src/models/Email.php +++ b/src/models/Email.php @@ -174,9 +174,7 @@ protected function defineRules(): array [ ['to'], 'required', - 'when' => static function($model) { - return $model->recipientType == EmailRecord::TYPE_CUSTOM; - }, + 'when' => static fn($model) => $model->recipientType == EmailRecord::TYPE_CUSTOM, ], [ [ diff --git a/src/models/LineItem.php b/src/models/LineItem.php index ee3c04faa2..1a8f701aa2 100755 --- a/src/models/LineItem.php +++ b/src/models/LineItem.php @@ -592,7 +592,6 @@ public function getFulfilledTotalQuantity(): int /** * Normalizes a purchasable’s validation rule. * - * @param mixed $rule * @param PurchasableInterface $purchasable * @return mixed */ @@ -930,6 +929,7 @@ private function _populateFromPurchasable(PurchasableInterface $purchasable): vo } $snapshot = [ + // @TODO move these to base purchasable on next breaking change 'price' => $purchasable->getPrice(), 'sku' => $purchasable->getSku(), 'description' => $purchasable->getDescription(), diff --git a/src/models/ProductType.php b/src/models/ProductType.php index 86167ca05a..54d478b50f 100644 --- a/src/models/ProductType.php +++ b/src/models/ProductType.php @@ -243,18 +243,16 @@ protected function defineRules(): array [ ['variantTitleFormat'], 'required', - 'when' => static function($model) { + 'when' => static fn($model) => /** @var static $model */ - return !$model->hasVariantTitleField; - }, + !$model->hasVariantTitleField, ], [ ['productTitleFormat'], 'required', - 'when' => static function($model) { + 'when' => static fn($model) => /** @var static $model */ - return !$model->hasProductTitleField; - }, + !$model->hasProductTitleField, ], [['name', 'handle', 'descriptionFormat'], 'string', 'max' => 255], [['handle'], UniqueValidator::class, 'targetClass' => ProductTypeRecord::class, 'targetAttribute' => ['handle'], 'message' => 'Not Unique'], diff --git a/src/models/ShippingRule.php b/src/models/ShippingRule.php index 84a912df75..d9535f9a55 100644 --- a/src/models/ShippingRule.php +++ b/src/models/ShippingRule.php @@ -12,6 +12,7 @@ use craft\commerce\base\Model; use craft\commerce\base\ShippingRuleInterface; use craft\commerce\base\StoreTrait; +use craft\commerce\elements\conditions\customers\ShippingRuleCustomerCondition; use craft\commerce\elements\conditions\orders\ShippingRuleOrderCondition; use craft\commerce\elements\Order; use craft\commerce\Plugin; @@ -123,6 +124,14 @@ class ShippingRule extends Model implements ShippingRuleInterface, HasStoreInter */ private ?ShippingRuleOrderCondition $_orderCondition = null; + /** + * @var ShippingRuleCustomerCondition|null + * @see setCustomerCondition() + * @see getCustomerCondition() + * @since 5.4.0 + */ + private ?ShippingRuleCustomerCondition $_customerCondition = null; + /** * @throws InvalidConfigException */ @@ -203,7 +212,7 @@ function($attribute) { } }, ], - [['id', 'orderCondition', 'description', 'storeId'], 'safe'], + [['id', 'customerCondition', 'orderCondition', 'description', 'storeId'], 'safe'], ]; } @@ -262,6 +271,41 @@ public function getOrderCondition(): ShippingRuleOrderCondition return $condition; } + /** + * @param ShippingRuleCustomerCondition|string|array|null $condition + * @return void + * @throws InvalidConfigException + * @since 5.4.0 + */ + public function setCustomerCondition(ShippingRuleCustomerCondition|string|array|null $condition): void + { + if (is_string($condition)) { + $condition = Json::decodeIfJson($condition); + } + + if (!$condition instanceof ShippingRuleCustomerCondition) { + $condition['class'] = ShippingRuleCustomerCondition::class; + $condition = Craft::$app->getConditions()->createCondition($condition); + /** @var ShippingRuleCustomerCondition $condition */ + } + $condition->forProjectConfig = false; + + $this->_customerCondition = $condition; + } + + /** + * @return ShippingRuleCustomerCondition + * @since 5.4.0 + */ + public function getCustomerCondition(): ShippingRuleCustomerCondition + { + $condition = $this->_customerCondition ?? new ShippingRuleCustomerCondition(); + $condition->mainTag = 'div'; + $condition->name = 'customerCondition'; + + return $condition; + } + /** * @inheritdoc */ diff --git a/src/models/Store.php b/src/models/Store.php index 1f05f17a25..1f1165a0fb 100644 --- a/src/models/Store.php +++ b/src/models/Store.php @@ -310,9 +310,7 @@ public function getSites(): Collection */ public function getSiteNames(): Collection { - return collect($this->getSites())->map(function(Site $site) { - return $site->getName(); - }); + return collect($this->getSites())->map(fn(Site $site) => $site->getName()); } /** @@ -682,7 +680,6 @@ public function getMinimumTotalPriceStrategy(bool $parse = true): string } /** - * @param mixed $countries * @return void * @throws DeprecationException * @throws InvalidConfigException @@ -775,8 +772,6 @@ public function getInventoryLocations(): Collection */ public function getInventoryLocationsOptions(): array { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($this->id)->map(function($location) { - return ['value' => $location->id, 'label' => $location->getUiLabel()]; - })->toArray(); + return Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($this->id)->map(fn($location) => ['value' => $location->id, 'label' => $location->getUiLabel()])->toArray(); } } diff --git a/src/models/StoreSettings.php b/src/models/StoreSettings.php index 3a78fd58e0..40dc31ffb2 100644 --- a/src/models/StoreSettings.php +++ b/src/models/StoreSettings.php @@ -53,7 +53,7 @@ class StoreSettings extends Model /** * @var ?ZoneAddressCondition */ - private ?ZoneAddressCondition $_marketAddressCondition; + private ?ZoneAddressCondition $_marketAddressCondition = null; /** * @inheritdoc @@ -174,13 +174,12 @@ public function getCountries(): array } /** - * @param mixed $countries * @return void * @throws InvalidConfigException */ public function setCountries(mixed $countries): void { - $countries = $countries ?? []; + $countries ??= []; $countries = Json::decodeIfJson($countries); if (!is_array($countries)) { @@ -196,9 +195,7 @@ public function setCountries(mixed $countries): void public function getCountriesList(): array { $all = Craft::$app->getAddresses()->getCountryRepository()->getList(Craft::$app->language); - return array_filter($all, function($fieldHandle) { - return in_array($fieldHandle, $this->getCountries(), true); - }, ARRAY_FILTER_USE_KEY); + return array_filter($all, fn($fieldHandle) => in_array($fieldHandle, $this->getCountries(), true), ARRAY_FILTER_USE_KEY); } /** diff --git a/src/models/TaxCategory.php b/src/models/TaxCategory.php index 2eae84f067..0dd364c523 100644 --- a/src/models/TaxCategory.php +++ b/src/models/TaxCategory.php @@ -157,9 +157,7 @@ protected function defineRules(): array return [ [['handle'], 'required'], [['handle'], UniqueValidator::class, 'targetClass' => TaxCategoryRecord::class], - [['handle'], HandleValidator::class, 'when' => function($model) use ($isStandardTaxEngine) { - return $isStandardTaxEngine; - }], + [['handle'], HandleValidator::class, 'when' => fn($model) => $isStandardTaxEngine], ]; } diff --git a/src/models/TaxRate.php b/src/models/TaxRate.php index 531dced6dd..4b0004e243 100644 --- a/src/models/TaxRate.php +++ b/src/models/TaxRate.php @@ -144,9 +144,7 @@ protected function defineRules(): array $rules[] = [ ['taxCategoryId'], 'required', - 'when' => function($model): bool { - return !in_array($model->taxable, TaxRateRecord::ORDER_TAXABALES, true); - }, + 'when' => fn($model): bool => !in_array($model->taxable, TaxRateRecord::ORDER_TAXABALES, true), ]; $rules[] = [[ 'code', diff --git a/src/models/TransferDetail.php b/src/models/TransferDetail.php index 87b0817a85..031ba7099d 100644 --- a/src/models/TransferDetail.php +++ b/src/models/TransferDetail.php @@ -81,9 +81,7 @@ public function setTransfer(Transfer $transfer): void public function defineRules(): array { return [ - [['quantity'], 'number', 'integerOnly' => true, 'min' => 1, 'max' => 99999, 'when' => function() { - return $this->getTransfer()->transferStatus === TransferStatusType::DRAFT; - }], + [['quantity'], 'number', 'integerOnly' => true, 'min' => 1, 'max' => 99999, 'when' => fn() => $this->getTransfer()->transferStatus === TransferStatusType::DRAFT], ]; } } diff --git a/src/records/Gateway.php b/src/records/Gateway.php index 6ba945da3a..e5771f7fc4 100644 --- a/src/records/Gateway.php +++ b/src/records/Gateway.php @@ -20,6 +20,7 @@ * @property int $id * @property bool $isArchived * @property string $name + * @property string $orderCondition * @property string $paymentType * @property array $settings * @property int $sortOrder diff --git a/src/records/ShippingMethod.php b/src/records/ShippingMethod.php index fa06dcf4d8..afabd4dca2 100644 --- a/src/records/ShippingMethod.php +++ b/src/records/ShippingMethod.php @@ -21,6 +21,7 @@ * @property int $storeId * @property string $name * @property array|string $orderCondition + * @property array|string $customerCondition * @property ShippingRule[] $rules * @author Pixel & Tonic, Inc. * @since 2.0 diff --git a/src/records/ShippingRule.php b/src/records/ShippingRule.php index 7b4a563932..8e9cce2df3 100644 --- a/src/records/ShippingRule.php +++ b/src/records/ShippingRule.php @@ -22,8 +22,8 @@ * @property int $methodId * @property string $orderConditionFormula * @property array|string $orderCondition + * @property array|string $customerCondition * @property float $maxRate - * @property float $minRate * @property string $name * @property float $percentageRate diff --git a/src/services/CatalogPricing.php b/src/services/CatalogPricing.php index 4ba3039695..1c66ebd974 100755 --- a/src/services/CatalogPricing.php +++ b/src/services/CatalogPricing.php @@ -307,7 +307,7 @@ public function generateCatalogPrices(?array $purchasableIds = null, ?array $cat */ public function getCatalogPrice(int $purchasableId, ?int $storeId = null, ?int $userId = null, bool $isPromotionalPrice = false): ?float { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $userKey = $userId ?? 'all'; $promoKey = $isPromotionalPrice ? 'promo' : 'standard'; $key = 'catalog-price-' . implode('-', [$storeId, $userKey, $promoKey]); @@ -333,7 +333,7 @@ public function getCatalogPrice(int $purchasableId, ?int $storeId = null, ?int $ */ public function getCatalogPricesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $allPriceRows = $this->createCatalogPricesQuery(storeId: $storeId, allPrices: true) // Override select to prevent `min`/grouping @@ -567,7 +567,7 @@ public function createCatalogPricingQuery(?int $userId = null, int|string|null $ ->from([Table::CATALOG_PRICING . ' cp']); // Use condition builder to tweak the query for reusability - $condition = $condition ?? Craft::$app->getConditions()->createCondition([ + $condition ??= Craft::$app->getConditions()->createCondition([ 'class' => CatalogPricingCondition::class, 'allPrices' => $allPrices, ]); @@ -624,7 +624,7 @@ public function createCatalogPricesQuery(?int $userId = null, int|string|null $s ->from([Table::CATALOG_PRICING . ' cp']); // Use condition builder to tweak the query for reusability - $condition = $condition ?? Craft::$app->getConditions()->createCondition([ + $condition ??= Craft::$app->getConditions()->createCondition([ 'class' => CatalogPricingCondition::class, 'allPrices' => $allPrices, ]); diff --git a/src/services/CatalogPricingRules.php b/src/services/CatalogPricingRules.php index 38eb35b61d..0b52447d54 100644 --- a/src/services/CatalogPricingRules.php +++ b/src/services/CatalogPricingRules.php @@ -78,7 +78,7 @@ public function getCatalogPricingRuleById(int $id, ?int $storeId = null): ?Catal */ public function getAllCatalogPricingRules(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allCatalogPricingRules === null || !isset($this->_allCatalogPricingRules[$storeId])) { $query = $this->_createCatalogPricingRuleQuery() @@ -106,7 +106,7 @@ public function getAllCatalogPricingRules(?int $storeId = null): Collection */ public function getAllCatalogPricingRulesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; // @TODO figure out if memoization is needed here $catalogPricingRules = $this->_createCatalogPricingRuleQuery() ->andWhere(['id' => (new Query()) @@ -137,10 +137,9 @@ public function getAllEnabledCatalogPricingRules(?int $storeId = null): Collecti */ public function getAllActiveCatalogPricingRules(?int $storeId = null): Collection { - return $this->getAllEnabledCatalogPricingRules($storeId)->where(function(CatalogPricingRule $pcr) { + return $this->getAllEnabledCatalogPricingRules($storeId)->where(fn(CatalogPricingRule $pcr) => // If there are no dates or rule is currently in the date range add it to the active list - return (($pcr->dateFrom === null || $pcr->dateFrom->getTimestamp() <= time()) && ($pcr->dateTo === null || $pcr->dateTo->getTimestamp() >= time())); - }); + ($pcr->dateFrom === null || $pcr->dateFrom->getTimestamp() <= time()) && ($pcr->dateTo === null || $pcr->dateTo->getTimestamp() >= time())); } /** @@ -150,9 +149,7 @@ public function getAllActiveCatalogPricingRules(?int $storeId = null): Collectio */ public function getAllCatalogPricingRulesWithUserConditions(?int $storeId = null): Collection { - return $this->getAllCatalogPricingRules($storeId)->where(function(CatalogPricingRule $pcr) { - return !empty($pcr->getCustomerCondition()->getConditionRules()); - }); + return $this->getAllCatalogPricingRules($storeId)->where(fn(CatalogPricingRule $pcr) => !empty($pcr->getCustomerCondition()->getConditionRules())); } /** @@ -340,6 +337,7 @@ protected function _createCatalogPricingRuleQuery(): ?Query 'enabled', 'id', 'isPromotionalPrice', + 'metadata', 'name', 'productCondition', 'purchasableCondition', @@ -366,10 +364,10 @@ protected function _clearCaches(): void protected function _createCatalogPricingRuleModels(array $rows): Collection { return collect($rows)->map(function($row) { - $row['customerCondition'] = $row['customerCondition'] ?? ''; - $row['productCondition'] = $row['productCondition'] ?? ''; - $row['purchasableCondition'] = $row['purchasableCondition'] ?? ''; - $row['variantCondition'] = $row['variantCondition'] ?? ''; + $row['customerCondition'] ??= ''; + $row['productCondition'] ??= ''; + $row['purchasableCondition'] ??= ''; + $row['variantCondition'] ??= ''; return Craft::createObject(CatalogPricingRule::class, ['config' => ['attributes' => $row]]); })->keyBy('id'); diff --git a/src/services/Currencies.php b/src/services/Currencies.php index 663e0e2bef..5918237245 100644 --- a/src/services/Currencies.php +++ b/src/services/Currencies.php @@ -74,9 +74,7 @@ public function getTeller(\Money\Currency|string $currency): Teller */ public function getCurrencyByIso(string $iso): ?\Money\Currency { - return $this->getAllCurrencies()->first(function(\Money\Currency $currency) use ($iso) { - return $currency->getCode() == $iso; - }); + return $this->getAllCurrencies()->first(fn(\Money\Currency $currency) => $currency->getCode() == $iso); } @@ -95,12 +93,10 @@ public function getAllCurrencies(): Collection */ public function getAllCurrenciesList(): array { - return $this->getAllCurrencies()->map(function($currency) { - return [ - 'label' => $currency->getCode(), // TODO get name somehow - 'value' => $currency->getCode(), - ]; - })->toArray(); + return $this->getAllCurrencies()->map(fn($currency) => [ + 'label' => $currency->getCode(), // TODO get name somehow + 'value' => $currency->getCode(), + ])->toArray(); } /** diff --git a/src/services/Discounts.php b/src/services/Discounts.php index b4d52841fc..bace3f3368 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -219,7 +219,7 @@ class Discounts extends Component */ public function getDiscountById(int $id, ?int $storeId = null): ?Discount { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; // Keep this as a query for the performance boost $discounts = $this->_createDiscountQuery() @@ -244,7 +244,7 @@ public function getDiscountById(int $id, ?int $storeId = null): ?Discount */ public function getAllDiscounts(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allDiscounts === null || !isset($this->_allDiscounts[$storeId])) { $discounts = $this->_createDiscountQuery() @@ -467,11 +467,33 @@ public function orderCouponAvailable(Order $order, string &$explanation = null): return false; } + if ($discount->hasOrderCondition() && !$discount->getOrderCondition()->matchElement($order)) { + $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order.'); + + return false; + } + + if ($discount->hasCustomerCondition() && (!$order->getCustomer() || !$discount->getCustomerCondition()->matchElement($order->getCustomer()))) { + $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to customer mismatch.'); + return false; + } + + if ($discount->hasShippingAddressCondition() && (!$order->getShippingAddress() || !$discount->getShippingAddressCondition()->matchElement($order->getShippingAddress()))) { + $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to address mismatch.'); + return false; + } + + if ($discount->hasBillingAddressCondition() && (!$order->getBillingAddress() || !$discount->getBillingAddressCondition()->matchElement($order->getBillingAddress()))) { + $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to address mismatch.'); + return false; + } + if (!$this->_isDiscountConditionFormulaValid($order, $discount)) { $explanation = Craft::t('commerce', 'Discount is not allowed for the order'); return false; } + if (!$this->_isDiscountDateValid($order, $discount)) { $explanation = Craft::t('commerce', 'Discount is out of date.'); return false; @@ -515,7 +537,7 @@ public function getDiscountByCode(?string $code, ?int $storeId = null): ?Discoun return null; } - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $query = $this->_createDiscountQuery()->where(['storeId' => $storeId]); $query->innerJoin(Table::COUPONS . ' coupons', '[[coupons.discountId]] = [[discounts.id]]'); @@ -530,12 +552,8 @@ public function getDiscountByCode(?string $code, ?int $storeId = null): ?Discoun return null; } - return ArrayHelper::firstWhere($this->_populateDiscounts($discounts), function(Discount $discount) use ($code) { - return ( - $discount->enabled && - ArrayHelper::contains($discount->getCoupons(), fn(Coupon $coupon) => strcasecmp($coupon->code, $code) === 0) - ); - }); + return ArrayHelper::firstWhere($this->_populateDiscounts($discounts), fn(Discount $discount) => $discount->enabled && + ArrayHelper::contains($discount->getCoupons(), fn(Coupon $coupon) => strcasecmp($coupon->code, $code) === 0)); } /** @@ -847,7 +865,7 @@ public function saveDiscount(Discount $model, bool $runValidation = true): bool foreach ($model->getPurchasableIds() as $purchasableId) { $relation = new DiscountPurchasableRecord(); $element = Craft::$app->getElements()->getElementById($purchasableId); - $relation->purchasableType = get_class($element); + $relation->purchasableType = $element::class; $relation->purchasableId = $purchasableId; $relation->discountId = $model->id; $relation->save(false); @@ -928,7 +946,7 @@ public function deleteDiscountById(int $id): bool public function ensureSortOrder(?int $storeId = null): void { // @TODO ensure sort order per store - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $table = Table::DISCOUNTS; @@ -1316,10 +1334,10 @@ private function _populateDiscounts(array $discounts): array $discount['purchasableIds'] = !empty($discount['purchasableIds']) ? Json::decodeIfJson($discount['purchasableIds'], true) : []; // IDs can be either category ID or entry ID due to the entryfication $discount['categoryIds'] = !empty($discount['categoryIds']) ? Json::decodeIfJson($discount['categoryIds'], true) : []; - $discount['orderCondition'] = $discount['orderCondition'] ?? ''; - $discount['customerCondition'] = $discount['customerCondition'] ?? ''; - $discount['billingAddressCondition'] = $discount['billingAddressCondition'] ?? ''; - $discount['shippingAddressCondition'] = $discount['shippingAddressCondition'] ?? ''; + $discount['orderCondition'] ??= ''; + $discount['customerCondition'] ??= ''; + $discount['billingAddressCondition'] ??= ''; + $discount['shippingAddressCondition'] ??= ''; $discount = Craft::createObject([ 'class' => Discount::class, diff --git a/src/services/Emails.php b/src/services/Emails.php index e2f836d162..1b38689c6b 100644 --- a/src/services/Emails.php +++ b/src/services/Emails.php @@ -232,7 +232,7 @@ public function getEmailById(int $id, ?int $storeId = null): ?Email */ public function getAllEmails(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allEmails === null || !isset($this->_allEmails[$storeId])) { $results = $this->_createEmailQuery() @@ -751,7 +751,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor if ($pdf->fileNameFormat) { try { $fileName = $view->renderObjectTemplate($pdf->fileNameFormat, $order); - } catch (\Throwable $e) { + } catch (\Throwable) { $fileName = $defaultFileName; } } diff --git a/src/services/Gateways.php b/src/services/Gateways.php index 6f8fbcea4b..40c3bb0270 100644 --- a/src/services/Gateways.php +++ b/src/services/Gateways.php @@ -34,7 +34,6 @@ use yii\base\InvalidConfigException; use yii\base\NotSupportedException; use yii\web\ServerErrorHttpException; -use function get_class; /** * Gateway service. @@ -111,7 +110,7 @@ public function getAllGatewayTypes(): array */ public function getAllCustomerEnabledGateways(): Collection { - return $this->getAllGateways()->where(fn(GatewayInterface $gateway) => $gateway->getIsFrontendEnabled()); + return $this->getAllGateways()->filter(fn(GatewayInterface $gateway) => $gateway->getIsFrontendEnabled()); } /** @@ -258,15 +257,7 @@ public function saveGateway(Gateway $gateway, bool $runValidation = true): bool if ($gateway->isArchived) { $configData = null; } else { - $configData = [ - 'name' => $gateway->name, - 'handle' => $gateway->handle, - 'type' => get_class($gateway), - 'settings' => $gateway->getSettings(), - 'sortOrder' => ($gateway->sortOrder ?? 99), - 'paymentType' => $gateway->paymentType, - 'isFrontendEnabled' => $gateway->getIsFrontendEnabled(false), - ]; + $configData = $gateway->getConfig(); } $configPath = self::CONFIG_GATEWAY_KEY . '.' . $gatewayUid; @@ -306,6 +297,7 @@ public function handleChangedGateway(ConfigEvent $event): void } $gatewayRecord->isFrontendEnabled = $data['isFrontendEnabled']; + $gatewayRecord->orderCondition = $data['orderCondition'] ?? null; $gatewayRecord->isArchived = false; $gatewayRecord->dateArchived = null; $gatewayRecord->uid = $gatewayUid; @@ -446,7 +438,7 @@ public function getGatewayOverrides(string $handle): ?array */ private function _createGatewayQuery(): Query { - return (new Query()) + $query = (new Query()) ->select([ 'dateArchived', 'handle', @@ -462,6 +454,14 @@ private function _createGatewayQuery(): Query ]) ->orderBy(['sortOrder' => SORT_ASC]) ->from([Table::GATEWAYS]); + + // TODO: remove after next breakpoint + $db = Craft::$app->getDb(); + if ($db->columnExists(Table::GATEWAYS, 'orderCondition')) { + $query->addSelect('orderCondition'); + } + + return $query; } /** diff --git a/src/services/InventoryLocations.php b/src/services/InventoryLocations.php index c5d4375c81..9a66af2df9 100644 --- a/src/services/InventoryLocations.php +++ b/src/services/InventoryLocations.php @@ -60,9 +60,7 @@ public function getAllInventoryLocations(bool $withTrashed = false): Collection */ public function getAllInventoryLocationsAsList(bool $withTrashed = false): array { - return $this->getAllInventoryLocations($withTrashed)->mapWithKeys(function(InventoryLocation $location) { - return [$location->id => $location->getUiLabel()]; - })->toArray(); + return $this->getAllInventoryLocations($withTrashed)->mapWithKeys(fn(InventoryLocation $location) => [$location->id => $location->getUiLabel()])->toArray(); } /** @@ -86,7 +84,7 @@ public function getInventoryLocationById(int $id, bool $withTrashed = false): ?I */ public function getInventoryLocations(?int $storeId = null, bool $withTrashed = false): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $locationIds = (new Query()) ->select(['inventoryLocationId']) @@ -96,9 +94,7 @@ public function getInventoryLocations(?int $storeId = null, bool $withTrashed = ->column(); // Keep the order of the locationIds - return $this->_getAllInventoryLocations($withTrashed)->whereIn('id', $locationIds)->sortBy(function($inventoryLocation) use ($locationIds) { - return array_search($inventoryLocation->id, $locationIds); - }); + return $this->_getAllInventoryLocations($withTrashed)->whereIn('id', $locationIds)->sortBy(fn($inventoryLocation) => array_search($inventoryLocation->id, $locationIds)); } /** diff --git a/src/services/LineItemStatuses.php b/src/services/LineItemStatuses.php index 35aa6d394c..ee360c199c 100644 --- a/src/services/LineItemStatuses.php +++ b/src/services/LineItemStatuses.php @@ -263,7 +263,7 @@ public function handleArchivedLineItemStatus(ConfigEvent $event): void */ public function getAllLineItemStatuses(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allLineItemStatuses === null || !isset($this->_allLineItemStatuses[$storeId])) { $results = $this->_createLineItemStatusesQuery() diff --git a/src/services/LineItems.php b/src/services/LineItems.php index 812c1994cd..52e945b769 100644 --- a/src/services/LineItems.php +++ b/src/services/LineItems.php @@ -11,6 +11,7 @@ use craft\commerce\db\Table; use craft\commerce\elements\Order; use craft\commerce\enums\LineItemType; +use craft\commerce\errors\LineItemNotFoundException; use craft\commerce\events\LineItemEvent; use craft\commerce\helpers\LineItem as LineItemHelper; use craft\commerce\models\LineItem; @@ -22,10 +23,10 @@ use craft\helpers\DateTimeHelper; use craft\helpers\Json; use craft\helpers\StringHelper; +use Exception; use LitEmoji\LitEmoji; use Throwable; use yii\base\Component; -use yii\base\Exception; use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; @@ -176,7 +177,7 @@ public function getAllLineItemsByOrderId(int $orderId): array * @return LineItem * @throws \Exception */ - public function resolveLineItem(Order $order, int $purchasableId, array $options = []): LineItem + public function resolveLineItem(Order $order, int $purchasableId, array $options = [], array $params = []): LineItem { $signature = LineItemHelper::generateOptionsSignature($options); @@ -191,7 +192,14 @@ public function resolveLineItem(Order $order, int $purchasableId, array $options if ($result) { $lineItem = new LineItem($result); } else { - $lineItem = $this->create($order, compact('purchasableId', 'options')); + $params = array_merge([ + 'qty' => 1, + 'options' => $options, + 'note' => '', + 'purchasableId' => $purchasableId, + ], $params); + + $lineItem = $this->create($order, $params); } return $lineItem; @@ -243,14 +251,13 @@ public function saveLineItem(LineItem $lineItem, bool $runValidation = true): bo { $isNewLineItem = !$lineItem->id; - if (!$lineItem->id) { + if ($isNewLineItem) { $lineItemRecord = new LineItemRecord(); } else { $lineItemRecord = LineItemRecord::findOne($lineItem->id); if (!$lineItemRecord) { - throw new Exception(Craft::t('commerce', 'No line item exists with the ID “{id}”', - ['id' => $lineItem->id])); + throw new LineItemNotFoundException('Line with ID ”' . $lineItem->id . '“ not found!'); } } @@ -263,7 +270,7 @@ public function saveLineItem(LineItem $lineItem, bool $runValidation = true): bo } if ($runValidation && !$lineItem->validate()) { - Craft::info('Line item not saved due to validation error.', __METHOD__); + Craft::info('Line Item not saved due to validation error(s).', __METHOD__); return false; } @@ -499,7 +506,7 @@ public function eagerLoadLineItemsForOrders(array $orders): array foreach ($lineItemsResults as $result) { $result['snapshot'] = Json::decodeIfJson($result['snapshot']); $lineItem = new LineItem($result); - $lineItems[$lineItem->orderId] = $lineItems[$lineItem->orderId] ?? []; + $lineItems[$lineItem->orderId] ??= []; $lineItems[$lineItem->orderId][] = $lineItem; } diff --git a/src/services/OrderAdjustments.php b/src/services/OrderAdjustments.php index d7875dcd7c..c412a44073 100644 --- a/src/services/OrderAdjustments.php +++ b/src/services/OrderAdjustments.php @@ -13,6 +13,7 @@ use craft\commerce\base\AdjusterInterface; use craft\commerce\db\Table; use craft\commerce\elements\Order; +use craft\commerce\errors\OrderAdjustmentNotFoundException; use craft\commerce\models\OrderAdjustment; use craft\commerce\Plugin; use craft\commerce\records\OrderAdjustment as OrderAdjustmentRecord; @@ -150,19 +151,20 @@ public function getAllOrderAdjustmentsByOrderId(int $orderId): array */ public function saveOrderAdjustment(OrderAdjustment $orderAdjustment, bool $runValidation = true): bool { - if ($orderAdjustment->id) { + $newAdjustment = !$orderAdjustment->id; + + if ($newAdjustment) { + $record = new OrderAdjustmentRecord(); + } else { $record = OrderAdjustmentRecord::findOne($orderAdjustment->id); if (!$record) { - throw new Exception(Craft::t('commerce', 'No order Adjustment exists with the ID “{id}”', - ['id' => $orderAdjustment->id])); + throw new OrderAdjustmentNotFoundException('Order Adjustment with ID ”' . $orderAdjustment->id . '“ not found!'); } - } else { - $record = new OrderAdjustmentRecord(); } if ($runValidation && !$orderAdjustment->validate()) { - Craft::info('Order Adjustment not saved due to validation error.', __METHOD__); + Craft::info('Order Adjustment not saved due to validation error(s).', __METHOD__); return false; } @@ -231,7 +233,7 @@ public function eagerLoadOrderAdjustmentsForOrders(array $orders): array $result['sourceSnapshot'] = Json::decodeIfJson($result['sourceSnapshot']); $adjustment = new OrderAdjustment($result); - $orderAdjustments[$adjustment->orderId] = $orderAdjustments[$adjustment->orderId] ?? []; + $orderAdjustments[$adjustment->orderId] ??= []; $orderAdjustments[$adjustment->orderId][] = $adjustment; } diff --git a/src/services/OrderNotices.php b/src/services/OrderNotices.php index a3cc5e18bf..278062524d 100644 --- a/src/services/OrderNotices.php +++ b/src/services/OrderNotices.php @@ -46,7 +46,7 @@ public function eagerLoadOrderNoticesForOrders(array $orders): array 'attributes' => $result, ]); - $orderNotices[$notice->orderId] = $orderNotices[$notice->orderId] ?? []; + $orderNotices[$notice->orderId] ??= []; $orderNotices[$notice->orderId][] = $notice; } diff --git a/src/services/OrderStatuses.php b/src/services/OrderStatuses.php index 6d9ea28eba..05aa964aa3 100644 --- a/src/services/OrderStatuses.php +++ b/src/services/OrderStatuses.php @@ -126,7 +126,7 @@ class OrderStatuses extends Component */ public function getAllOrderStatuses(?int $storeId = null, bool $withTrashed = false): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allOrderStatuses === null || !isset($this->_allOrderStatuses[$storeId])) { $results = $this->_createOrderStatusesQuery(true) @@ -224,7 +224,7 @@ public function getDefaultOrderStatusForOrder(Order $order): ?OrderStatus */ public function getOrderCountByStatus(?int $storeId = null): array { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $countGroupedByStatusId = (new Query()) ->select(['[[o.orderStatusId]]', 'count(o.id) as orderCount']) diff --git a/src/services/PaymentCurrencies.php b/src/services/PaymentCurrencies.php index 5ced29a86c..0829396946 100644 --- a/src/services/PaymentCurrencies.php +++ b/src/services/PaymentCurrencies.php @@ -52,7 +52,7 @@ class PaymentCurrencies extends Component */ public function getPaymentCurrencyById(int $id, ?int $storeId = null): ?PaymentCurrency { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $all = $this->getAllPaymentCurrencies($storeId); @@ -69,7 +69,7 @@ public function getPaymentCurrencyById(int $id, ?int $storeId = null): ?PaymentC */ public function getAllPaymentCurrencies(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allPaymentCurrencies === null || !isset($this->_allPaymentCurrencies[$storeId])) { $results = $this->_createPaymentCurrencyQuery() @@ -110,7 +110,7 @@ public function getAllPaymentCurrencies(?int $storeId = null): Collection */ public function getPaymentCurrencyByIso(string $iso, ?int $storeId = null): ?PaymentCurrency { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; return $this->getAllPaymentCurrencies($storeId)->firstWhere('iso', $iso); } @@ -131,13 +131,11 @@ public function getPrimaryPaymentCurrencyIso(?int $storeId = null): string */ public function getPrimaryPaymentCurrency(?int $storeId = null): ?PaymentCurrency { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); - return $this->getAllPaymentCurrencies($storeId)->firstWhere(function(PaymentCurrency $currency) use ($storeCurrency) { - return $currency->getCode() == $storeCurrency->getCode(); - }); + return $this->getAllPaymentCurrencies($storeId)->firstWhere(fn(PaymentCurrency $currency) => $currency->getCode() == $storeCurrency->getCode()); } /** @@ -151,9 +149,7 @@ public function getNonPrimaryPaymentCurrencies(?int $storeId = null): Collection { $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); - return $this->getAllPaymentCurrencies($storeId)->where(function(PaymentCurrency $currency) use ($storeCurrency) { - return $currency->getCode() != $storeCurrency->getCode(); - }); + return $this->getAllPaymentCurrencies($storeId)->where(fn(PaymentCurrency $currency) => $currency->getCode() != $storeCurrency->getCode()); } /** @@ -277,7 +273,7 @@ public function deletePaymentCurrencyById(int $id): bool private function _getExchange(?int $storeId = null) { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); $nonPrimaryCurrencies = $this->getNonPrimaryPaymentCurrencies($storeId)->mapWithKeys(fn(PaymentCurrency $currency) => [$currency->iso => (string)$currency->rate]); @@ -308,7 +304,7 @@ public function convertAmount(Money $amount, Currency|string $currency, ?int $st $currency = new Currency($currency); } - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; $fromPaymentCurrency = $this->getPaymentCurrencyByIso($amount->getCurrency(), $storeId); $toPaymentCurrency = $this->getPaymentCurrencyByIso($currency, $storeId); diff --git a/src/services/Pdfs.php b/src/services/Pdfs.php index 27741b2d70..f2b371ecac 100644 --- a/src/services/Pdfs.php +++ b/src/services/Pdfs.php @@ -216,7 +216,7 @@ class Pdfs extends Component */ public function getAllPdfs(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allPdfs === null || !isset($this->_allPdfs[$storeId])) { $results = $this->_createPdfsQuery() diff --git a/src/services/Purchasables.php b/src/services/Purchasables.php index a40d9cb532..e7b9629700 100644 --- a/src/services/Purchasables.php +++ b/src/services/Purchasables.php @@ -268,7 +268,7 @@ public function getPurchasableById(int $purchasableId, ?int $siteId = null, int| return $this->_purchasableById->get($purchasableId); } - $siteId = $siteId ?? Craft::$app->getSites()->getCurrentSite()->id; + $siteId ??= Craft::$app->getSites()->getCurrentSite()->id; $elementType = Craft::$app->getElements()->getElementTypeById($purchasableId); if ($elementType === null || !class_exists($elementType)) { diff --git a/src/services/Sales.php b/src/services/Sales.php index 940fb87c28..1d16a0da2e 100644 --- a/src/services/Sales.php +++ b/src/services/Sales.php @@ -32,7 +32,6 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\db\StaleObjectException; -use function get_class; use function in_array; /** @@ -614,7 +613,7 @@ public function saveSale(Sale $model, bool $runValidation = true): bool $relation = new SalePurchasableRecord(); $relation->purchasableId = $purchasableId; $purchasable = Craft::$app->getElements()->getElementById($purchasableId, null, null, ['trashed' => null]); - $relation->purchasableType = get_class($purchasable); + $relation->purchasableType = $purchasable::class; $relation->saleId = $model->id; $relation->save(); diff --git a/src/services/ShippingCategories.php b/src/services/ShippingCategories.php index 5c8a553cb0..ff0282aaf4 100644 --- a/src/services/ShippingCategories.php +++ b/src/services/ShippingCategories.php @@ -52,7 +52,7 @@ class ShippingCategories extends Component */ public function getAllShippingCategories(?int $storeId = null, bool $withTrashed = false): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allShippingCategories === null || !isset($this->_allShippingCategories[$storeId])) { $results = $this->_createShippingCategoryQuery(true) @@ -93,9 +93,7 @@ public function getAllShippingCategoriesAsList(?int $storeId = null): array { $categories = $this->getAllShippingCategories($storeId); - return $categories->mapWithKeys(function(ShippingCategory $category) { - return [$category->id => $category->name]; - })->all(); + return $categories->mapWithKeys(fn(ShippingCategory $category) => [$category->id => $category->name])->all(); } /** diff --git a/src/services/ShippingMethods.php b/src/services/ShippingMethods.php index 444880b2b5..1231c62c64 100644 --- a/src/services/ShippingMethods.php +++ b/src/services/ShippingMethods.php @@ -72,7 +72,7 @@ class ShippingMethods extends Component */ public function getAllShippingMethods(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allShippingMethods === null || !isset($this->_allShippingMethods[$storeId])) { $results = $this->_createShippingMethodQuery() @@ -149,9 +149,7 @@ public function getMatchingShippingMethods(Order $order): array } // Sort by price. Using the cached price and don't call `$method->getPriceForOrder($order);` again. - uasort($matchingMethods, static function($a, $b) { - return $a['price'] <=> $b['price']; - }); + uasort($matchingMethods, static fn($a, $b) => $a['price'] <=> $b['price']); $shippingMethods = []; foreach ($matchingMethods as $shippingMethod) { @@ -224,6 +222,7 @@ public function saveShippingMethod(ShippingMethod $model, bool $runValidation = $record->name = $model->name; $record->handle = $model->handle; $record->orderCondition = $model->getOrderCondition()->getConfig(); + $record->customerCondition = $model->getCustomerCondition()->getConfig(); $record->enabled = $model->enabled; $record->validate(); @@ -287,6 +286,7 @@ private function _createShippingMethodQuery(): Query 'id', 'name', 'orderCondition', + 'customerCondition', 'storeId', ]) ->from([Table::SHIPPINGMETHODS]); diff --git a/src/services/ShippingRules.php b/src/services/ShippingRules.php index e3fe34cd88..81484b09a3 100644 --- a/src/services/ShippingRules.php +++ b/src/services/ShippingRules.php @@ -53,7 +53,7 @@ public function getAllShippingRules(): Collection $allShippingRules = []; foreach ($results as $result) { - $result['orderCondition'] = $result['orderCondition'] ?? ''; + $result['orderCondition'] ??= ''; $allShippingRules[] = Craft::createObject([ 'class' => ShippingRule::class, 'attributes' => $result, @@ -128,6 +128,7 @@ public function saveShippingRule(ShippingRule $model, bool $runValidation = true } $record->orderCondition = $model->getOrderCondition()->getConfig(); + $record->customerCondition = $model->getCustomerCondition()->getConfig(); if (empty($record->priority) && empty($model->priority)) { $count = ShippingRuleRecord::find()->where(['methodId' => $model->methodId])->count(); @@ -225,6 +226,7 @@ private function _createShippingRulesQuery(): Query 'shippingrules.name', 'shippingrules.orderConditionFormula', 'shippingrules.orderCondition', + 'shippingrules.customerCondition', 'shippingrules.percentageRate', 'shippingrules.perItemRate', 'shippingrules.priority', diff --git a/src/services/ShippingZones.php b/src/services/ShippingZones.php index 6326e49a03..32c0212f0f 100644 --- a/src/services/ShippingZones.php +++ b/src/services/ShippingZones.php @@ -43,7 +43,7 @@ class ShippingZones extends Component */ public function getAllShippingZones(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allZones === null || !isset($this->_allZones[$storeId])) { $results = $this->_createQuery()->where(['storeId' => $storeId])->all(); diff --git a/src/services/TaxRates.php b/src/services/TaxRates.php index 4deda7df01..75458289ae 100644 --- a/src/services/TaxRates.php +++ b/src/services/TaxRates.php @@ -45,7 +45,7 @@ class TaxRates extends Component */ public function getAllTaxRates(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allTaxRates === null || !isset($this->_allTaxRates[$storeId])) { $results = $this->_createTaxRatesQuery() diff --git a/src/services/TaxZones.php b/src/services/TaxZones.php index 4016cdc8be..58c62bdad2 100644 --- a/src/services/TaxZones.php +++ b/src/services/TaxZones.php @@ -45,7 +45,7 @@ class TaxZones extends Component */ public function getAllTaxZones(?int $storeId = null): Collection { - $storeId = $storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; + $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; if ($this->_allZones === null || !isset($this->_allZones[$storeId])) { $results = $this->_createQuery() diff --git a/src/services/Transactions.php b/src/services/Transactions.php index d63b108710..55d5ba4b51 100644 --- a/src/services/Transactions.php +++ b/src/services/Transactions.php @@ -515,7 +515,7 @@ public function eagerLoadTransactionsForOrders(array $orders): array foreach ($transactionResults as $result) { $transaction = new Transaction($result); - $transactions[$transaction->orderId] = $transactions[$transaction->orderId] ?? []; + $transactions[$transaction->orderId] ??= []; $transactions[$transaction->orderId][] = $transaction; } diff --git a/src/stats/RepeatCustomers.php b/src/stats/RepeatCustomers.php index 8e72772a97..bd1482c890 100644 --- a/src/stats/RepeatCustomers.php +++ b/src/stats/RepeatCustomers.php @@ -39,9 +39,7 @@ public function getData(): array ->column(); - $repeat = count(array_filter($repeatRows, static function($row) { - return $row > 1; - })); + $repeat = count(array_filter($repeatRows, static fn($row) => $row > 1)); $percentage = round($total ? ($repeat / $total) * 100 : 0); diff --git a/src/taxidvalidators/EuVatIdValidator.php b/src/taxidvalidators/EuVatIdValidator.php index b33326aa7c..1a3f892a61 100644 --- a/src/taxidvalidators/EuVatIdValidator.php +++ b/src/taxidvalidators/EuVatIdValidator.php @@ -2,8 +2,8 @@ namespace craft\commerce\taxidvalidators; +use Craft; use craft\commerce\base\TaxIdValidatorInterface; -use DvK\Vat\Validator; /** * EuVatIdValidator checks if a given VAT ID is valid in the EU. @@ -13,26 +13,103 @@ */ class EuVatIdValidator implements TaxIdValidatorInterface { - private Validator $_vatValidator; + public const API_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number'; - public function __construct() - { - $this->_vatValidator = new Validator(); - } + /** + * Regular expression patterns per country code + * + * @var array + * @link http://ec.europa.eu/taxation_customs/vies/faq.html?locale=lt#item_11 + */ + private array $_patterns = [ + 'AT' => 'U[A-Z\d]{8}', + 'BE' => '(0|1)\d{9}', + 'BG' => '\d{9,10}', + 'CY' => '\d{8}[A-Z]', + 'CZ' => '\d{8,10}', + 'DE' => '\d{9}', + 'DK' => '(\d{2} ?){3}\d{2}', + 'EE' => '\d{9}', + 'EL' => '\d{9}', + 'ES' => '([A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8})', + 'EU' => '\d{9}', + 'FI' => '\d{8}', + 'FR' => '[A-Z\d]{2}\d{9}', + 'GB' => '(\d{9}|\d{12}|(GD|HA)\d{3})', + 'HR' => '\d{11}', + 'HU' => '\d{8}', + 'IE' => '((\d{7}[A-Z]{1,2})|(\d[A-Z]\d{5}[A-Z]))', + 'IT' => '\d{11}', + 'LT' => '(\d{9}|\d{12})', + 'LU' => '\d{8}', + 'LV' => '\d{11}', + 'MT' => '\d{8}', + 'NL' => '\d{9}B\d{2}', + 'PL' => '\d{10}', + 'PT' => '\d{9}', + 'RO' => '\d{2,10}', + 'SE' => '\d{12}', + 'SI' => '\d{8}', + 'SK' => '\d{10}', + 'SM' => '\d{5}', + ]; public static function displayName(): string { return \Craft::t('commerce', 'EU VAT ID'); } + private function _splitNumber(string $idNumber): array + { + $vatNumber = strtoupper($idNumber); + $country = substr($vatNumber, 0, 2); + $number = substr($vatNumber, 2); + + return [$country, $number]; + } + public function validateFormat(string $idNumber): bool { - return $this->_vatValidator->validateFormat($idNumber); + [$country, $number] = $this->_splitNumber($idNumber); + + if (!isset($this->_patterns[$country])) { + return false; + } + + return preg_match('/^' . $this->_patterns[$country] . '$/', $number) > 0; } public function validateExistence(string $idNumber): bool { - return $this->_vatValidator->validateExistence($idNumber); + [$country, $number] = $this->_splitNumber($idNumber); + + try { + $client = Craft::createGuzzleClient(); + $response = $client->post(self::API_URL, [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'countryCode' => $country, + 'vatNumber' => $number, + ]), + ]); + + $responseBody = json_decode($response->getBody(), true); + if ($response->getStatusCode() !== 200) { + return false; + } + + if (!isset($responseBody['valid']) || $responseBody['valid'] !== true) { + return false; + } + + return true; + } catch (\Exception $e) { + \Craft::error($e->getMessage(), __METHOD__); + } + + return false; } /** @@ -46,7 +123,7 @@ public static function isEnabled(): bool public function validate(string $idNumber): bool { try { - return $this->_vatValidator->validate($idNumber); + return $this->validateFormat($idNumber) && $this->validateExistence($idNumber); } catch (\Exception $e) { \Craft::error('Error validating EU VAT ID: ' . $e->getMessage()); return false; diff --git a/src/templates/orders/_edit.twig b/src/templates/orders/_edit.twig index d380635bb4..928099a9f7 100644 --- a/src/templates/orders/_edit.twig +++ b/src/templates/orders/_edit.twig @@ -21,6 +21,7 @@ "Address 1", "Address 2", "Address 3", + "Address copied to user.", "Adjustments", "Amount", "Are you sure you want to complete this order?", @@ -160,6 +161,7 @@ "Unable to retrieve load cart URL", "Unit Price", "Update order", + "User", "View customer", "View user", "Web", diff --git a/src/templates/orders/_history.twig b/src/templates/orders/_history.twig index b792435252..2deaa3bfc4 100644 --- a/src/templates/orders/_history.twig +++ b/src/templates/orders/_history.twig @@ -1,8 +1,6 @@ {# Order History Tab #}