6.x - #4124
Draft
lukeholder wants to merge 207 commits into
Draft
Conversation
lukeholder
marked this pull request as draft
September 23, 2025 07:42
….5 requirements - Rename src/ to src-yii2/ for the legacy Yii2 codebase. The new src/ directory will contain Laravel-based CraftCms\Commerce code, introduced progressively in later commits. - Bump composer requirements to Craft 6 (craftcms/cms 6.0.0-alpha.1) and PHP 8.5+. - Update phpstan, rector, and .gitignore for the new layout. This is a structural change only — no behaviour changes.
Element and base-class signature compatibility for Craft 6's new abstract method signatures: - Order::getRecalculationMode() — return null safely before init() runs - Order::getLink() — return type ?\Illuminate\Support\HtmlString - Product/Variant/Subscription::setEagerLoadedElements() — use \CraftCms\Cms\Element\Data\EagerLoadPlan - Transfer::prepareEditScreen() — return \CraftCms\Cms\Http\Responses\CpScreenResponse|Response - VariantCollection::make() — variadic, matching Illuminate\Collection - Purchasable::__unset() — add string type hint and void return - PaymentCurrency::safeAttributes() — declare array return type Rector pass: remove redundant /** @inheritdoc */ docblocks across src-yii2/; add #[\Override] in src/gql/ handlers; drop deprecated setAccessible(true) calls from tests (redundant since PHP 8.1).
The Commerce debug panel relied on craft\debug\Module (Yii2 debug module) which no longer exists in Craft 6. Removed entirely: - src-yii2/debug/CommercePanel.php - src-yii2/helpers/DebugPanel.php - src-yii2/events/CommerceDebugPanelDataEvent.php - src-yii2/views/debug/commerce/ (detail, model, summary views) - _registerDebugPanels() and its onInit hook from Plugin.php - All DebugPanel::prependOrAppendModelTab() calls from 19 controllers Also wires up craft.commerce as a macro on CraftCms\Cms\Twig\Variables\ CraftVariable so it works with the Laravel-based Twig variable layer, and migrates Plugin to the new CraftCms\Cms\Support\Facades\Updates facade.
Introduce Pest as the test runner for new src/ code, alongside the existing Codeception suite (which stays in place while src-yii2/ is still active). New tests will live under tests/Unit and tests/Feature following Pest conventions; src-yii2/ tests stay Codeception until their corresponding classes are migrated. Adds: - tests/Pest.php — Pest bootstrap - tests/TestCase.php, tests/UnitTestCase.php — base classes - tests/Support/DatabaseLock.php — concurrency helper for parallel runs - testbench.yaml — Orchestra Testbench config - phpunit.xml.dist — Pest/PHPUnit config (Composer dependency bumps for Pest/Testbench are folded into the bootstrap commit.)
Move the dependency-free constants/enums to the new src/ tree first; every later stage can then import from CraftCms\Commerce\. New locations: - craft\commerce\db\Table → CraftCms\Commerce\Database\Table - craft\commerce\enums\InventoryTransactionType → CraftCms\Commerce\Inventory\Enums\InventoryTransactionType - craft\commerce\enums\InventoryUpdateQuantityType → CraftCms\Commerce\Inventory\Enums\InventoryUpdateQuantityType - craft\commerce\enums\LineItemType → CraftCms\Commerce\Order\LineItem\Enums\LineItemType - craft\commerce\enums\TransferStatusType → CraftCms\Commerce\Transfer\Enums\TransferStatusType Legacy classes become class_alias stubs that point at the new locations, preserving backwards compatibility for existing imports.
Move the 11 plugin contracts to domain-organized Contracts/ namespaces. With Stage 1 enums and these interfaces in place, the rest of the migration can implement against the new types without touching the old craft\commerce\base\* paths. New locations: - craft\commerce\base\AdjusterInterface → CraftCms\Commerce\Order\Adjuster\Contracts\AdjusterInterface - craft\commerce\base\CatalogPricingConditionRuleInterface → CraftCms\Commerce\CatalogPricing\Contracts\CatalogPricingConditionRuleInterface - craft\commerce\base\GatewayInterface → CraftCms\Commerce\Payment\Gateway\Contracts\GatewayInterface - craft\commerce\base\HasStoreInterface → CraftCms\Commerce\Store\Contracts\HasStoreInterface - craft\commerce\base\InventoryMovementInterface → CraftCms\Commerce\Inventory\Contracts\InventoryMovementInterface - craft\commerce\base\PlanInterface → CraftCms\Commerce\Subscription\Contracts\PlanInterface - craft\commerce\base\PurchasableInterface → CraftCms\Commerce\Purchasable\Contracts\PurchasableInterface - craft\commerce\base\RequestResponseInterface → CraftCms\Commerce\Payment\Gateway\Contracts\RequestResponseInterface - craft\commerce\base\ShippingMethodInterface → CraftCms\Commerce\Shipping\Contracts\ShippingMethodInterface - craft\commerce\base\ShippingRuleInterface → CraftCms\Commerce\Shipping\Contracts\ShippingRuleInterface - craft\commerce\base\StatInterface → CraftCms\Commerce\Stats\Contracts\StatInterface Legacy interfaces become class_alias stubs.
Move all 56 event classes from craft\commerce\events into the new domain-organized CraftCms\Commerce\*\Events namespaces. Event classes adopt PHP 8 constructor property promotion for clean, typed initialization. New locations group events by domain: - CraftCms\Commerce\Catalog\Events - CraftCms\Commerce\Email\Events - CraftCms\Commerce\Inventory\Events - CraftCms\Commerce\Order\Events - CraftCms\Commerce\Payment\Events - CraftCms\Commerce\Pdf\Events - CraftCms\Commerce\Promotion\Events - CraftCms\Commerce\Purchasable\Events - CraftCms\Commerce\Report\Events - CraftCms\Commerce\Shipping\Events - CraftCms\Commerce\Store\Events - CraftCms\Commerce\Subscription\Events - CraftCms\Commerce\Tax\Events Cancelable events (previously extending craft\events\CancelableEvent) now use the CraftCms\Cms\Shared\Concerns\ValidatableEvent trait. Legacy craft\commerce\events\* classes are replaced with class_alias stubs pointing at the new classes.
Move the 11 helper classes from craft\commerce\helpers to CraftCms\Commerce\Helpers, swapping internal Craft/Yii static helper calls for CraftCms\Cms\* and Laravel equivalents (Url, Cp, Json, StringHelper, etc.). New locations: - CraftCms\Commerce\Helpers\Cp - CraftCms\Commerce\Helpers\Currency - CraftCms\Commerce\Helpers\Gql - CraftCms\Commerce\Helpers\LineItem - CraftCms\Commerce\Helpers\Locale - CraftCms\Commerce\Helpers\Localization - CraftCms\Commerce\Helpers\Order - CraftCms\Commerce\Helpers\PaymentForm - CraftCms\Commerce\Helpers\ProductQuery - CraftCms\Commerce\Helpers\ProjectConfigData - CraftCms\Commerce\Helpers\Purchasable class_alias stubs in src-yii2/helpers/ are deferred until the services that depend on these helpers are migrated; the old craft\commerce\helpers\* paths still work via the Yii2 autoloader.
Migrate the simplest models (scalar properties, no element ties) from craft\commerce\models to domain-organized classes under src/. New classes extend CraftCms\Cms\Component\Component and use getRules() with Laravel validation syntax instead of Yii2's defineRules(). Models: - craft\commerce\models\Coupon → CraftCms\Commerce\Promotion\Models\Coupon - craft\commerce\models\LineItemStatus → CraftCms\Commerce\Order\Models\LineItemStatus - craft\commerce\models\PaymentCurrency → CraftCms\Commerce\Payment\Models\PaymentCurrency - craft\commerce\models\PurchasableStore → CraftCms\Commerce\Purchasable\Models\PurchasableStore - craft\commerce\models\Settings → CraftCms\Commerce\Settings - craft\commerce\models\ShippingCategory → CraftCms\Commerce\Shipping\Models\ShippingCategory - craft\commerce\models\TaxCategory → CraftCms\Commerce\Tax\Models\TaxCategory Subscription/payment forms and gateway response models: - craft\commerce\models\subscriptions\CancelSubscriptionForm → CraftCms\Commerce\Subscription\Forms\CancelSubscriptionForm - craft\commerce\models\subscriptions\SubscriptionForm → CraftCms\Commerce\Subscription\Forms\SubscriptionForm - craft\commerce\models\subscriptions\SwitchPlansForm → CraftCms\Commerce\Subscription\Forms\SwitchPlansForm - craft\commerce\models\subscriptions\SubscriptionPayment → CraftCms\Commerce\Subscription\Models\SubscriptionPayment - craft\commerce\models\responses\Dummy → CraftCms\Commerce\Payment\Gateway\Responses\Dummy - craft\commerce\models\responses\Manual → CraftCms\Commerce\Payment\Gateway\Responses\Manual - craft\commerce\models\responses\DummySubscriptionResponse → CraftCms\Commerce\Subscription\Responses\DummySubscriptionResponse Legacy classes become class_alias stubs.
Models with lazy-loaded relations (typically via getter methods that call into a service to fetch related models) move into domain-organized classes under src/. Models: - craft\commerce\models\OrderNotice → CraftCms\Commerce\Order\Models\OrderNotice - craft\commerce\models\OrderHistory → CraftCms\Commerce\Order\Models\OrderHistory - craft\commerce\models\SiteStore → CraftCms\Commerce\Store\Models\SiteStore - craft\commerce\models\ShippingRuleCategory → CraftCms\Commerce\Shipping\Models\ShippingRuleCategory Payment forms: - craft\commerce\models\payments\BasePaymentForm → CraftCms\Commerce\Payment\Forms\BasePaymentForm - craft\commerce\models\payments\OffsitePaymentForm → CraftCms\Commerce\Payment\Forms\OffsitePaymentForm - craft\commerce\models\payments\CreditCardPaymentForm → CraftCms\Commerce\Payment\Forms\CreditCardPaymentForm - craft\commerce\models\payments\DummyPaymentForm → CraftCms\Commerce\Payment\Forms\DummyPaymentForm CreditCardPaymentForm's Luhn check is converted from a Yii2 method validator to a Laravel closure rule in getRules(); setAttributes() now overrides the Validates trait's method for expiry parsing. Legacy classes become class_alias stubs.
adjustments, and inventory movement models Stage 5c — inventory items, catalog/product type sites, transfer details: - ProductTypeSite → CraftCms\Commerce\Catalog\Models\ProductTypeSite - InventoryItem → CraftCms\Commerce\Inventory\Models\InventoryItem - InventoryFulfillmentLevel → CraftCms\Commerce\Inventory\Models\InventoryFulfillmentLevel - InventoryLevel → CraftCms\Commerce\Inventory\Models\InventoryLevel - InventoryTransaction → CraftCms\Commerce\Inventory\Models\InventoryTransaction - UpdateInventoryLevel → CraftCms\Commerce\Inventory\Models\UpdateInventoryLevel - UpdateInventoryLevelInTransfer → CraftCms\Commerce\Inventory\Models\UpdateInventoryLevelInTransfer - TransferDetail → CraftCms\Commerce\Transfer\Models\TransferDetail Stage 5d — email, PDF, zones, adjustments, inventory movements, plus shared infrastructure: - Email → CraftCms\Commerce\Email\Models\Email - Pdf → CraftCms\Commerce\Pdf\Models\Pdf - OrderAdjustment → CraftCms\Commerce\Order\Models\OrderAdjustment - TaxRate → CraftCms\Commerce\Tax\Models\TaxRate - ShippingAddressZone → CraftCms\Commerce\Shipping\Models\ShippingAddressZone - TaxAddressZone → CraftCms\Commerce\Tax\Models\TaxAddressZone - base\Zone (abstract) → CraftCms\Commerce\Base\Zone - base\InventoryMovement (abstract) + 6 InventoryMovement subclasses + DeactivateInventoryLocation → CraftCms\Commerce\Inventory\Models\ Adds CraftCms\Commerce\Store\Concerns\StoreTrait — shared storeId helper for store-aware models. Updates InventoryItemTrait, InventoryLocationTrait, and InventoryMovementInterface to reference new namespaces. Old src-yii2/Base/StoreTrait marked @deprecated. Legacy classes become class_alias stubs.
…logPricing Four models from craft\commerce\models move to domain-organized classes under src/: - OrderStatus → CraftCms\Commerce\Order\Models\OrderStatus - PaymentSource → CraftCms\Commerce\Payment\Models\PaymentSource - InventoryLocation → CraftCms\Commerce\Inventory\Models\InventoryLocation - CatalogPricing → CraftCms\Commerce\Catalog\Models\CatalogPricing Key swaps: - Cp::statusLabelHtml() → app(CraftCms\Cms\Cp\Html\StatusHtml::class) - Html::encode() → htmlspecialchars(..., ENT_QUOTES | ENT_SUBSTITUTE) - Db::uidsByIds() → DB::table(...)->uidsByIds() (Laravel query builder macro) - craft\elements\Address → CraftCms\Cms\Address\Elements\Address - craft\base\* contracts → CraftCms\Cms\Component\Contracts\* - Craft::$app->getUser()->getIdentity()?->can() → request()->craftUser()?->can() - HandleValidator → inline regex + reserved-word closure - CurrencyAttributeBehavior dropped (Yii2-only) - Craft::$app->getDeprecator() → CraftCms\Cms\Support\Facades\Deprecator Legacy classes become class_alias stubs.
4 supporting interfaces Three models: - Sale → CraftCms\Commerce\Promotion\Models\Sale - StoreSettings → CraftCms\Commerce\Store\Models\StoreSettings - Transaction → CraftCms\Commerce\Payment\Models\Transaction Four interfaces previously left at the legacy base/ path: - base\TaxIdValidatorInterface → CraftCms\Commerce\Tax\Contracts\TaxIdValidatorInterface - base\TaxEngineInterface → CraftCms\Commerce\Tax\Contracts\TaxEngineInterface - base\ZoneInterface → CraftCms\Commerce\Base\ZoneInterface - base\SubscriptionResponseInterface → CraftCms\Commerce\Subscription\Contracts\SubscriptionResponseInterface Key swaps: - new Query()->select()->from()->leftJoin()->where()->column() → DB::table()->leftJoin()->where()->pluck()->all() (Sale) - Craft::$app->getFormatter()->asPercent() → I18N::getFormatter()->asPercent() - Craft::$app->getAddresses()->getCountryRepository()->getList(language) → Addresses::getCountryRepository()->getList(app()->getLocale()) - Address::findOne($id) → Elements::getElementById($id, Address::class) - Craft::$app->getElements()->saveElement() → Elements::saveElement() - Conditions::createCondition() facade - Transaction's hash generation moved from init() to __construct - CurrencyAttributeBehavior dropped (Yii2-only) Legacy classes become class_alias stubs.
The full shipping method class hierarchy moves to src/: - craft\commerce\base\ShippingMethod (abstract) → CraftCms\Commerce\Shipping\Models\BaseShippingMethod - craft\commerce\models\ShippingMethod → CraftCms\Commerce\Shipping\Models\ShippingMethod - craft\commerce\models\ShippingMethodOption → CraftCms\Commerce\Shipping\Models\ShippingMethodOption Key swaps: - craft\base\Chippable/Colorable/Iconic/Statusable → CraftCms\Cms\Component\Contracts\* - craft\enums\Color → CraftCms\Cms\Shared\Enums\Color - NotImplementedException → \BadMethodCallException (inline) - UniqueValidator → Rule::unique() (Laravel validation) - AttributeTypecastBehavior dropped (Yii2-only) - CurrencyAttributeBehavior / currencyAttributes() / getCurrency() dropped from ShippingMethodOption (Yii2-only) - Json::decodeIfJson() → CraftCms\Cms\Support\Json::decodeIfJson() - Conditions::createCondition() facade ShippingMethodOrderCondition and ShippingMethodCustomerCondition remain on the old craft\commerce\elements\conditions\* paths until their dependencies are migrated. Legacy classes become class_alias stubs.
craft\commerce\models\ShippingRule → CraftCms\Commerce\Shipping\Models\ShippingRule. Key swaps: - Json::decodeIfJson() → CraftCms\Cms\Support\Json::decodeIfJson() - Conditions::createCondition() facade - Yii2 attribute-based closure validators (addError()) → Laravel closures with $fail() pattern - validateShippingRuleCategories method validator → inline closure in getRules() using the Validates trait's addModelErrors() helper - $this->getAttributes() in getOptions() → $this->toArray() ShippingRuleOrderCondition and ShippingRuleCustomerCondition stay on the old craft\commerce\elements\conditions\* paths until their dependencies are migrated. Order and ShippingRuleCategory record references also stay on the old paths. Legacy class becomes a class_alias stub.
CatalogPricingRule moves to src/: - craft\commerce\models\CatalogPricingRule → CraftCms\Commerce\Catalog\Models\CatalogPricingRule Key swaps: - craft\base\Model → CraftCms\Cms\Component\Component - Yii2 defineRules() → Laravel getRules() with Rule::in() for 'apply' - I18N::getFormatter()->asPercent() / Conditions::createCondition() facades - CraftCms\Cms\Support\Json::decodeIfJson() Post-Stage 5 fixes: - Fix infinite recursion in ShippingMethodOrderCondition, ShippingRuleOrderCondition, DiscountOrderCondition config() methods. $this->toArray(['storeId']) was calling getObjectVars() which triggers the PHP 8.4 $config property hook getter, recursing into config(). Replaced with explicit ['storeId' => $this->storeId]. Also adds CraftCms\Commerce\Base\EnumHelpersTrait (companion to the Stage 1 enums, missed at the time) and the WIP changelog covering stages 1–5. Legacy CatalogPricingRule becomes a class_alias stub.
- Switch ConditionRule::modifyQuery() param types from craft\elements\db\ElementQueryInterface | yii\db\QueryInterface to Illuminate\Contracts\Database\Query\Builder, matching the Laravel condition rule signature in 6.x. - Affected: DiscountedItemSubtotalConditionRule, OrderCurrencyValuesAttributeConditionRule, OrderSiteConditionRule, ShippingMethodConditionRule. - src-yii2/services/Taxes.php: minor adjustment alongside the above. - Templates: guard discounts/sales _edit.twig against a crash when the shippingrulecategories table doesn't exist yet (Stage 1/2 setups).
Settings already lived at CraftCms\Commerce\Settings from Stage 5a, but the legacy src-yii2/models/Settings.php still held the full Yii2 implementation. Now: - src-yii2/models/Settings.php replaced with a class_alias stub - src/Settings.php gains the setAttributes() override from the legacy class so deprecated Commerce-4 settings keys are silently stripped (preserves backward compatibility for project configs that still reference orderPdfFilenameFormat, autoSetNewCartAddresses, etc.). DummyPlan moves to CraftCms\Commerce\Subscription\Models\DummyPlan. Still extends the unmigrated craft\commerce\base\Plan; switches to the new CraftCms\Commerce\Subscription\Contracts\PlanInterface argument type. Legacy classes become class_alias stubs.
craft\commerce\models\Store → CraftCms\Commerce\Store\Models\Store.
Key swaps:
- craft\base\Model → CraftCms\Cms\Component\Component
- craft\helpers\App::parseEnv() → CraftCms\Cms\Support\Env::parse()
- craft\helpers\App::parseBooleanEnv() → CraftCms\Cms\Support\Env::parseBoolean()
- craft\helpers\UrlHelper::cpUrl() → CraftCms\Cms\Support\Url::cpUrl()
- craft\models\Site → CraftCms\Cms\Site\Data\Site
- UniqueValidator → Rule::unique() on the stores table, ignoring the
current record id
- Yii2 attribute-based closure validator for currency-change-when-
orders-exist → Laravel closure rule with $fail() pattern
- Craft::$app->getDeprecator() → CraftCms\Cms\Support\Facades\Deprecator
- Craft::t('commerce', ...) → global t() with category
- Yii2 attributes() override (added name/settings) → fields() override
(same purpose under the new serialization layer)
- Dropped EnvAttributeParserBehavior — the existing getXxx(bool $parse)
pattern already handles env parsing on every accessor
ZoneAddressCondition, Order element, and the Store record stay on the
legacy craft\commerce\* paths until those are migrated.
Legacy class becomes a class_alias stub.
…iscount Migrated craft\commerce\models\Discount → CraftCms\Commerce\Promotion\Models\Discount. Key swaps: - craft\base\Model → CraftCms\Cms\Component\Component - Yii2 Query builder (relation loaders) → DB::table()->leftJoin()->pluck()->all() - Conditions::createCondition() facade - I18N::getFormatter()->asPercent() - CraftCms\Cms\Support\Json::decodeIfJson() - Yii2 defineRules() → Laravel getRules(); closure validators rewritten with the $fail() pattern; Rule::in() for categoryRelationshipType/appliedTo - craft\elements\conditions\ElementConditionInterface → CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface - DiscountOrderCondition / DiscountCustomerCondition / DiscountAddressCondition, Order element, DiscountRecord, Coupons service retained as legacy refs Removed 5.x-deprecated API while migrating (per CLAUDE.md guidance): - Discount::setExcludeOnSale() / getExcludeOnSale() (use $excludeOnPromotion) - Settings::VIEW_URI_CUSTOMERS / VIEW_URI_PROMOTIONS / VIEW_URI_SHIPPING / VIEW_URI_TAX constants - Store::setCountries() / getCountries() / getCountriesList() / getAdministrativeAreasListByCountryCode() / getMarketAddressCondition() (use the equivalents on Store::getSettings()) Legacy Discount becomes a class_alias stub.
craft\commerce\services\Currencies → CraftCms\Commerce\Services\Currencies.
This is the first service migrated under the new Craft 6 pattern (see
docs/6.x/extend/services.md): services are plain auto-loadable PHP
classes marked with #[\Illuminate\Container\Attributes\Singleton] so
Laravel's container reuses the instance. No Yii2 Component inheritance
on the new class. Preferred access:
app(\CraftCms\Commerce\Services\Currencies::class)->getTeller(...)
Legacy access stays working via the existing
`Plugin::getInstance()->getCurrencies()` route — the old service in
src-yii2/ is reduced to a thin Yii2 Component that delegates every
method to the new singleton via app(). Once all callers move to app(),
the legacy wrapper can be deleted.
Behaviour is unchanged. init() moved to __construct(). Tellers are still
cached per-iso on the singleton.
…e\Services craft\commerce\services\PaymentCurrencies → CraftCms\Commerce\Services\PaymentCurrencies. #[Singleton] on the new class, plain PHP. Yii2 component declaration in Plugin.php stays — the legacy wrapper at src-yii2/services/ PaymentCurrencies.php now delegates every method to the new singleton via app(). Key swaps inside the new service: - Yii2 craft\db\Query → Laravel DB::table()->select()->where()->get() - Db::update(...) → DB::table()->where(...)->update(...) - Craft::createObject(['class' => ..., 'attributes' => ...]) → new PaymentCurrency((array) $row) - craft\commerce\errors\CurrencyException → \RuntimeException (the Yii2 base exception isn't visible to phpstan / no longer relevant in the Laravel layer) Removed convertCurrency() from the new service — deprecated in 5.0.0. Kept on the legacy wrapper only, so the two unmigrated src-yii2/ callers (Order element, OrdersController) keep working; they'll move to convert()/convertAmount() when their classes migrate.
…rvices craft\commerce\services\TaxCategories → CraftCms\Commerce\Services\TaxCategories. Key swaps: - Yii2 Query → Laravel DB::table() + Schema facade for the icon/color column-exists check (replaces $db->getSchema()->getTableSchema()->getColumn()) - ArrayHelper::firstWhere/firstValue/map/getColumn → collect()->firstWhere/ first/mapWithKeys/pluck() - Craft::$app->getDb()->createCommand()->delete()/insert() → DB::table()-> where()->delete() / DB::table()->insert() - Craft::$app->getQueue()->push(new ResaveElements([...])) (Yii2 array-config job) → dispatch(new ResaveElements(elementType: ..., criteria: ...)) (CraftCms\Cms\Element\Jobs\ResaveElements) - Yii2 InvalidConfigException for "must have one default" → \RuntimeException TaxCategoryRecord and softDelete() retained — the Yii2 record stays until the records layer migrates. Legacy class becomes a delegating Yii2 Component wrapper.
…ce\Services craft\commerce\services\ShippingCategories → CraftCms\Commerce\Services\ShippingCategories. Same patterns as TaxCategories: - #[Singleton] on the new class, Plugin's Yii2 component declaration delegates via the wrapper at src-yii2/services/ - Yii2 Query → Laravel DB::table()/Schema facade - ArrayHelper utilities → Collection / native array_diff - Craft::$app->getQueue()->push(new ResaveElements([...])) → dispatch(new ResaveElements(elementType: ..., criteria: ..., updateSearchIndex: false)) - InvalidConfigException for "must have one default" → \RuntimeException ShippingCategoryRecord, softDelete(), and the legacy Variant element are retained. The purchasable-store fallback logic on product-type removal (assigns affected purchasables to the default shipping category) is preserved exactly. Legacy class becomes a delegating Yii2 Component wrapper.
craft\commerce\services\TaxZones → CraftCms\Commerce\Services\TaxZones.
Same pattern as the previous 6a services: #[Singleton] new class,
delegating Yii2 Component wrapper at src-yii2/services/.
Swaps:
- Yii2 Query → Laravel DB::table()->select()->orderBy()
- Craft::createObject(['class' => TaxAddressZone, 'attributes' => $row])
→ new TaxAddressZone((array) $row)
- yii\base\Exception ("zone not found") → \RuntimeException
TaxZoneRecord, ZoneAddressCondition, and the legacy Plugin::getInstance()
->getStores()->getCurrentStore() lookup retained.
…Services craft\commerce\services\ShippingZones → CraftCms\Commerce\Services\ShippingZones. Mirrors the TaxZones migration: Yii2 Query → Laravel DB::table(), Craft::createObject → new ShippingAddressZone((array) $row), yii\base\Exception → \RuntimeException. Legacy class becomes a delegating Yii2 Component wrapper. This finishes Stage 6a (Store config services): Currencies, PaymentCurrencies, TaxCategories, ShippingCategories, TaxZones, ShippingZones — all behind app(CraftCms\Commerce\Services\* ::class).
Adds a "Stage 6a" section covering Currencies, PaymentCurrencies, TaxCategories, ShippingCategories, TaxZones, ShippingZones — all six now under CraftCms\Commerce\Services and accessed via app(). Captures the cross-cutting swaps applied (Yii2 Query → DB::table(), createObject → new, ArrayHelper → Collection, etc.). Also records that PaymentCurrencies::convertCurrency() (deprecated in 5.0.0) was dropped from the new service but kept on the legacy wrapper for the two unmigrated src-yii2/ callers.
…s to Laravel in src/
Ports craft\commerce\elements\actions\{CopyLoadCartUrl,CreateDiscount,CreateSale,
DownloadOrderPdfAction,SetDefaultVariant,UpdateOrderStatus}, the remaining
fieldlayoutelements\* classes (ProductTitleField, VariantTitleField, VariantsField,
UserAddressSettings, and the 8 Purchasable*Field classes), and the fields\Products/
Variants custom field types to CraftCms\Commerce\*, following the established
element-action/field-layout-element/relation-field base classes. Legacy classes
become class_alias stubs; Product/Variant/UpdateOrderStatus/SetDefaultVariant move
into src/Plugin.php's $elementTypes/$fieldTypes arrays, replacing the legacy
Fields::EVENT_REGISTER_FIELD_TYPES bridge in src-yii2/Plugin.php.
Also deletes src-yii2/linktypes/Product.php, which was fully superseded by the
already-migrated and already-registered CraftCms\Commerce\Catalog\LinkTypes\ProductLinkType
and had no remaining references anywhere in the codebase.
Replaces the manual per-driver SQL string building in Stat::getChartQueryOptionsByInterval() (MySQL CONVERT_TZ/PostgreSQL AT TIME ZONE/SQLite passthrough for timezone conversion, then EXTRACT/strftime/DATE() for day and month grouping keys) and the selectRaw()/groupByRaw()/ orderByRaw()/DB::raw() calls scattered across the individual Stats classes (SUM, COUNT, IFNULL vs COALESCE, CASE WHEN) with typed, composable query builder expressions. Adds four small expression classes under src/Support/Expressions/ (LocalTimestamp, DateOnly, MonthKey, Round) following the same driver-detection pattern as tpetry/laravel-query-expressions, for the SQL constructs the package doesn't provide (calendar month/day truncation with timezone conversion) — used together with the package's own Sum, Count, Coalesce, CaseGroup/CaseRule, Alias, and Value expressions everywhere else. Behavior-preserving: all Stats feature tests pass except the one pre-existing, unrelated TotalOrdersTest failure that predates this change.
…ling cms-6 checkout phpstan.neon pointed scanFiles/stubFiles at ../cms-6/yii2-adapter/..., which only resolves when cms-6 is checked out as a sibling directory (true in the local ddev dev setup, where /tmp/packages/cms-6 and /tmp/packages/commerce-6 are siblings, but not in CI or any standalone checkout of this repo). Point them at vendor/craftcms/yii2-adapter/... instead, which composer already installs identically everywhere and matches how every other CI job in this repo already resolves craftcms/cms and craftcms/yii2-adapter. Also pin parallel.maximumNumberOfProcesses to 1. Discovered while testing the above: PHPStan's worker processes each independently reflect on classes reached through class_alias() chains (the legacy src-yii2/ -> src/ stubs), and depending on which worker resolves a given alias target first, whether it can trace the chain is non-deterministic between otherwise-identical runs — surfacing as argument.type/method.notFound errors, or stale @phpstan-ignore-next-line suppressions, that flip between two or three call sites in Payment/Gateway/Gateways.php and Payment/Transactions.php from run to run. Single-process analysis is slower but removes the race entirely; confirmed 3 consecutive clean runs locally after pinning it.
Completes the Gateways domain migration: Gateway.php (inlining GatewayTrait), the three concrete gateway types, and the helpers/PaymentForm.php stub, with all consumers rewired to the new namespace. Fixes the CI phpstan failures caused by craft\commerce\base\Gateway still being a real, non-aliased class with legacy type-hints that mismatched GatewayInterface, and removes ~19 now-stale @phpstan-ignore-next-line suppressions this made provable.
2.4.5's PHPStanContainerMemento reflected into a private $container property on PHPStan\Parser\RichParser that no longer exists as of PHPStan 2.2.x, crashing rector process with MissingPrivatePropertyException. 2.6.3 requires phpstan/phpstan ^2.2.6, which includes the matching container-compatibility fix (rectorphp/rector#8208).
PR #4124 (head 6.x, base 5.x) stays open for the whole migration, so every push already triggers a pull_request run. The push: branches: [6.x] trigger was firing a second, redundant full CI run for the same commit.
Stores::afterDeleteCraftSiteHandler(): on a single-store install, reassigning the primary store to "another" store after the last site is deleted found no other store (firstWhere() returned null) and then dereferenced it. Skip the reassignment when there's nothing to promote. ProductTypes::getViewableProductTypeIds()/getCreatableProductTypeIds(): both called $user->can(...) without checking that request()->craftUser() returned a user, fataling in console/queue contexts. Mirrors the console/null-user guard already used by the sibling getViewableProductTypes().
…namespace composer.json maps craft\commerce\ to src-yii2/, and every file in these directories declares (or aliases into) namespace craft\commerce\base or craft\commerce\enums (lowercase). The directories were Base/ and Enums/ (capitalized), which macOS's case-insensitive filesystem silently tolerates but a case-sensitive Linux filesystem does not. This broke autoloading for every class under craft\commerce\base\* (StoreTrait, GatewayTrait, Model, Stat, etc.) and craft\commerce\enums\* on GitHub Actions CI runners, which never surfaced locally because ddev mounts the Mac host filesystem into the Linux container. It also explains why the Tests/Feature and Tests/Arch CI jobs have effectively never run to completion until now — they were gated behind Rector, which was itself broken until the previous commit.
… live autoload root
Two arch rules, both scoped to src/ and verified to actually catch violations
(Pest's toUse()/toBeUsedIn() resolve targets as classes/namespaces via its
ObjectsRepository, so plain function names outside its small hardcoded
core-language-construct list, and Class::method static-call strings, are
silently never matched — confirmed empirically before relying on either):
- No debug functions (die/dd/dump/env) in src/.
- src/ must not reference legacy Craft core classes, excluding craft\commerce
(allowed during the migration per this repo's CLAUDE.md).
Getting any arch() rule to run at all required moving
src-yii2/test/{fixtures/elements/ProductFixture.php,mockclasses/Purchasable.php}
to tests-yii2/, matching where every other pre-Pest reference/porting fixture
already lives. Pest's arch plugin enumerates every PSR-4 namespace declared in
composer.json to build its analysis graph, not just the expect() target, so
these two forgotten files sitting inside the live craft\commerce\ (src-yii2/)
autoload root — referencing craft\base\ElementInterface, which isn't even
aliased anymore — fataled the whole test run before either rule could
evaluate. Updated their two internal namespace declarations and the three
call sites that imported them (craft\commerce\test\* -> craftcommercetests\*)
to match their new location.
…rce\Gql\...) Same class of bug as the earlier src-yii2/Base and src-yii2/Enums fix, this time in the new src/ tree: namespace CraftCms\Commerce\Gql\Handlers (etc.) declared throughout, but the directory was src/gql/handlers (lowercase). Unlike the earlier StoreTrait case, this didn't fatal — Plugin::boot() registers CraftCms\Commerce\Gql\Handlers\HasProduct as a GQL argument handler via is_a($handler, ArgumentHandlerInterface::class, true), and is_a() with $allow_string swallows a failed autoload and just returns false. That surfaced as "Argument handler [...] must implement [ArgumentHandlerInterface]" on Tests/Feature CI, which was misleading: the class autoloads fine on macOS (case-insensitive host filesystem mounted into the ddev container) and so genuinely does implement the interface — it just couldn't be found by name on GitHub's case-sensitive Linux runners. Renamed src/gql -> src/Gql, handlers -> Handlers, types -> Types, types/input -> Types/Input, types/input/criteria -> Types/Input/Criteria. Re-scanned both src/ and src-yii2/ in full for any other namespace/directory casing mismatches; none remain.
…ueries, arguments)
Completes the GraphQL migration checklist: Arguments/Elements/{Product,Variant},
Interfaces/Elements/{Product,Variant}, Types/Elements/{Product,Variant},
Types/Generators/{ProductType,VariantType}, Types/Input/{IntFalse,Product,Variant},
Types/SaleType, Resolvers/Elements/{Product,Variant}, and Queries/{Product,Variant},
all under CraftCms\Commerce\Gql\. helpers/Gql.php was already fully ported in an
earlier commit; its legacy src-yii2 counterpart is now a thin deprecated subclass
matching the established pattern.
The "wire up schema-registration" checklist item turned out to already be mostly
done — Plugin.php already registered the GqlArguments handlers and the
GqlSchemaComponentsResolving/GqlEagerLoadableFieldsResolving listeners. The only
missing piece was populating the new $gqlTypes/$gqlQueries properties the base
Plugin class's HasGql concern reads automatically, replacing the legacy
Event::on(Gql::EVENT_REGISTER_GQL_TYPES/QUERIES) wiring entirely.
Also fixes 3 stray legacy craft\gql\* imports in already-migrated files
(Catalog/Variants.php, Gql/Types/Input/Criteria/{Product,Variant}Relation.php)
that the new Arch tests would otherwise have flagged, and points those two
Criteria classes at the new Arguments classes instead of the legacy ones.
Verified beyond phpstan/check-cs: manually executed a GraphQL query through
products -> variants -> sales against a seeded schema, and prebuilt/validated
the full schema (introspection path), both via a throwaway test since this
repo has no GQL feature-test harness yet.
…on on SQLite
Two compounding bugs, both in date/timezone handling around the "today" stat:
1. LocalTimestamp's SQLite branch returned the raw (UTC) column unconverted,
on the assumption "SQLite is only used by the test suite, which always
runs in UTC" — false: tests/TestCase.php pins the app's timezone to
America/Los_Angeles for determinism. Day/month grouping (TotalOrders and
every other stat using getChartQueryOptionsByInterval) would bucket orders
under their UTC calendar date instead of the app's configured one,
splitting a single day's data across two chart entries whenever UTC and
LA disagreed on the current date (i.e. for most of each 24-hour period).
Fixed by resolving the configured timezone's current UTC offset in PHP
(DateTimeZone::getOffset(), which accounts for DST) and applying it via
SQLite's datetime(column, '+/-N minutes') modifier, since SQLite has no
named-timezone SQL functions to call directly.
2. Separately, TotalOrdersTest's "today" dataset computed its expected
start/end dates via new DateTime('now') inside the ->with() array. Pest
resolves dataset closures before beforeEach()/app boot, i.e. before
TestCase::setUp() pins the timezone — so the dataset's "now" could be
read under a different default timezone than the one Stat itself later
uses when it independently recomputes "today" inside TotalOrders's own
constructor. Moved the date computation into the test body, after the
fixture (and app boot) has already run, so both sides agree by construction.
Verified with 5 repeated runs of the previously-100%-reproducible failure,
the full Stats suite, and the full test suite (109/109 passing, first fully
clean run this session).
Ports the five remaining Yii2 console controllers to CraftCms\Commerce\Console\Commands\*,
following the Illuminate\Console\Command + CraftCommand pattern already established by
the resave commands. GatewaysController's two real actions (list, webhook-url) split into
separate command classes, matching how cms-6 splits its own multi-action controllers
(e.g. project-config:get/set/apply). Legacy `commerce/*` CLI routes are preserved as
command aliases via $aliases, so existing scripts/muscle-memory keep working.
Destructive/interactive flows were translated to their Laravel-idiomatic equivalents where
that's a strict improvement with no behavior change for the required inputs: ResetData now
uses ConfirmableTrait (a --force bypass plus a components->task()-driven, single
DB::transaction()-wrapped delete, rather than the ad-hoc yes/no string prompt + manual
begin/commit/rollBack); ExampleTemplates and TransferCustomerData use Laravel's own
ask()/confirm() and only prompt for options not already supplied on the command line.
console/Controller.php (an empty pass-through base with nothing else extending it once the
above landed) is deleted outright, not stubbed — console controllers are CLI entry points
invoked by route string, not classes anything else in the codebase instantiates or
type-hints against, so there's no back-compat surface to preserve the way there is for
services/models.
Verified against a real dev install (not just phpstan/tests), which surfaced two real,
unrelated pre-existing bugs the migrated code paths hadn't exercised before:
- Gateway::set{Billing,Shipping}AddressCondition() didn't accept null, despite handling it
in the method body (setOrderCondition already did) - throws the moment any real gateway
config has a null condition, i.e. immediately for `commerce:gateways:list`.
- CatalogPricing::setQueueProgress() called method_exists(null, ...), which throws a
TypeError under PHP 8's stricter argument types - hits every no-queue call, i.e.
immediately for `commerce:pricing-catalog:generate`.
Both fixed. commerce:reset-data was verified by code review (DB::transaction wrapping,
correct table/column names) rather than executed live, since the harness's destructive-
action guard correctly declined to run a bulk-delete command even against a dev database
confirmed to have zero rows in every affected table.
Follow-up to 0a53428: a bad pathspec in that commit's `git add` (src-yii2/console, already handled by a separate git rm) silently aborted staging every other file passed in the same invocation, so Plugin.php's $commands registration for the 5 new console commands, the Gateway.php/CatalogPricing.php bugfixes those commands surfaced, and the CHANGELOG-WIP.md entry never actually made it into that commit — only the new command classes and the src-yii2 deletions did. The commands existed but were never wired up. Re-verified with phpstan and the full test suite now that Plugin.php's registration is actually included.
…ectConfigData,Purchasable}.php
All 8 already had complete src/Helpers/ counterparts from earlier migration work,
so this is mostly the usual legacy-stub conversion + consumer rewiring — but
verifying each one caught two real, latent bugs:
- src/Helpers/{Cp,Currency,Purchasable}.php still imported the legacy core
craft\helpers\Cp instead of CraftCms\Cms\Cp\FormFields (fieldHtml/moneyInputHtml/
textHtml) or the CraftCms\Cms template()/TemplateMode helpers (renderTemplate)
or FormFields::lightswitchFromConfig()->toHtml() (lightswitchHtml, itself
deprecated in cms-6's own yii2-adapter). This is a real gap in the "src/ should
not reference legacy Craft core classes" Arch rule added earlier this session:
Pest-arch's dependency-layer resolution excludes vendor-directory namespaces
entirely, so any craft\* class living under vendor/craftcms/cms (as opposed to
this repo's own src-yii2/) is invisible to it and can slip through undetected.
That's a separate, wider-reaching finding worth its own follow-up pass.
- CraftCms\Commerce\Helpers\Localization no longer `extends \craft\helpers\
Localization` (it only needs normalizePercentage(), and normalizeNumber() is
called via CraftCms\Cms\Support\Facades\I18N internally), but
PaymentsController.php called the commerce subclass's *inherited*
normalizeNumber() directly - undefined once the extends was dropped. Pointed
it at the I18N facade instead, matching Localization's own internal usage.
Rewired the 15 already-migrated src/ consumers that still imported the legacy
craft\commerce\helpers\* classes to use CraftCms\Commerce\Helpers\* directly.
Verified beyond phpstan/tests: called the FormFields-backed methods
(Cp::taxZoneFieldHtml, Currency::moneyInputHtml, Purchasable::skuInputHtml,
Purchasable::availableForPurchaseInputHtml) against a real dev install via
tinker, since none of the existing test suite exercises CP-rendering helpers.
…idgetTrait,StoreTrait,TaxEngineInterface,TaxIdValidatorInterface,ZoneInterface} All 9 already had complete src/ counterparts from earlier migration work (most already carried @deprecated docblocks pointing at them, and the CHANGELOG-WIP.md entries for their replacements already existed - this was purely the stub-conversion + verification pass). StatTrait is deliberately left as a real, untouched legacy trait, matching the GatewayTrait precedent from the Gateway migration: its properties were merged directly onto the new Stat class rather than ported to a dedicated trait, so there's no clean class_alias target, and it costs nothing to leave as free-standing legacy code for any third-party code still using it directly. Model.php is a genuine dead end: an empty pass-through subclass of craft\base\Model with zero consumers anywhere in the codebase. Pointed its stub at CraftCms\Cms\Component\Component directly (the documented modern base for what used to extend craft\base\Model - see docs/6.x/extend/models.md and every already-migrated Commerce model) rather than a nonexistent "new commerce Model" class. Comparing old vs new caught one real fidelity gap: legacy Stat implements both StatInterface and HasStoreInterface, but the new class only implemented StatInterface even though StoreTrait already satisfies HasStoreInterface's contract - just a dropped `implements` clause. Added it back. Rewired the 2 already-migrated src/ consumers (Catalog/Elements/Product.php, Store/Models/SiteStore.php) still importing the legacy craft\commerce\base\StoreTrait.
…ias TypeError race in service delegation layer
Wires the product/variant/transfer element-edit screens to the generic
EditElementController in routes/cp.php, moves Variables::getDonation() to
Plugin::getDonation(), and deletes LegacyRoutingModule.php (dead code - no
controllers remain in craft\commerce\controllers for it to resolve).
Also fixes a TypeError that any craft\commerce\services\* delegation method
returning/accepting a craft\commerce\{models,elements}\* class alias could
throw on its first call in a process (class_alias's own autoload racing the
return-type check). All 22 affected wrappers now type-hint against the
CraftCms\Commerce\* class directly, and Transfer::canView()/canSave()/
canDelete() now check the permission that's actually registered
(commerce-manageInventoryTransfers, not commerce-manageTransfers).
…ce/settings/producttypes
ProductTypesController::productTypeIndex()/editProductType() never passed
readOnly to their templates even though index.twig/_edit.twig both reference
it, throwing a Twig RuntimeError. productTypeIndex() also wrapped
index.twig's own `{% extends "commerce/_layouts/settings" %}` in a
CpScreenResponse, double-rendering the CP chrome - switched it to
pageTemplate() like every other index-listing controller (OrderStatuses,
Pdfs, etc.), since editProductType()'s _edit.twig is content-only and
CpScreenResponse is correct there.
…to native Laravel ShouldQueue jobs The 3 legacy Yii2-style jobs (already deleted in the previous commit) are replaced by src/Email/Jobs/SendEmailJob.php, src/Catalog/Jobs/ ResaveProductVariantsJob.php, and src/CatalogPricing/Jobs/CatalogPricingJob.php, each extending CraftCms\Cms\Queue\Job. All 3 call sites now use Job::dispatch() instead of craft\helpers\Queue::push(), so Commerce no longer routes any job through the yii2-adapter's LegacyJobWrapper shim. CatalogPricingJob widens Job::setProgress() to public since CatalogPricing::generateCatalogPrices() reports progress on its $queue argument via duck typing.
…lidator to the new Ruleset system Commerce 6.0 only ships Install.php for fresh installs, so the 6 loose point-release migrations drafted in database/migrations/ are unnecessary: 4 are schema changes already present in Install.php's table definitions (catalog pricing queue table, orders.customerDeleted, the subscriptions userId FK cascade, order notices.noticeType), and 2 are one-time data repairs (product type permission renaming, allVariants->variants changedattributes cleanup) for bugs that don't exist in a fresh 6.0 schema. CouponsValidator.php (which nothing referenced anymore) is replaced by a closure rule on Coupon::getRules()['code'], checking case-insensitive uniqueness against every other coupon row. It validates per-coupon inside Coupons::saveDiscountCoupons()'s save loop rather than batch-checking the whole Discount.coupons array like the legacy validator did, since each coupon is validated then immediately persisted before the next one in the loop - which naturally catches same-batch duplicates too, without needing the legacy version's own-discountId exclusion workaround.
…ugin convention HasTranslations checks dirname(getBasePath()).'/lang' before falling back to src/translations, so plugin translations belong at the package root. Stripped the old copyright header from each locale file to match core's app.php style.
…, changelog Previous commit's git add -A failed silently on a stale pathspec before it could stage the header strip and the crowdin.yml/.codecov.yml/changelog updates, so they landed in the working tree but not the commit.
Replaces the StoreBehavior-dependent getGlobals() lookup with
Sites::getCurrentSite()->getStore(), the Site::macro('getStore', ...)
registered in Plugin::registerBehaviorMacros(). CP asset bundles and Twig
templates stay in src-yii2/ for now (separate, larger migration task).
Confirmed via cms-6's yii2-adapter (Yii2ServiceProvider::registerCraftVariableCompatibility()) that the legacy CraftVariable EVENT_INIT bridge only forwards getComponents(), not attached behaviors, so this class never reached the live craft Twig global under Craft 6. craft.commerce/ orders/products/variants are already fully served by the NewCraftVariable macros in src/Plugin.php. Verified live via TemplateManager::renderString() before and after removal.
The alias was left over from when this file needed to disambiguate from the legacy craft\web\twig\variables\CraftVariable; that legacy import (and the CraftVariableBehavior it backed) is gone now, so there's no collision. Also fixed a stale docblock describing the pre-deletion legacy mechanism.
…umers, add opt-in serialization fields Site/User/Address no longer support attachBehavior(), so StoreBehavior, StoreLocationBehavior, ValidateOrganizationTaxIdBehavior, CurrencyAttributeBehavior, CustomerBehavior, and CustomerAddressBehavior have been dead/orphaned code since earlier migration stages stripped their EVENT_DEFINE_BEHAVIORS wiring. All six are deleted; their functionality was already independently reimplemented via macros and event listeners registered in src/Plugin.php. Auditing CurrencyAttributeBehavior's *AsCurrency consumers surfaced two live regressions (Transaction and CatalogPricing were both missing getters their own callers already used, throwing UnknownPropertyException on the order-edit and Settings > Product Pricing CP screens) and a third-party BC gap (ShippingMethodOption/ OrderAdjustment were public API via the legacy behavior's magic __call, independent of Commerce's own in-repo callers) — all four now have explicit get*AsCurrency() getters matching the pattern already established for Order/LineItem/Purchasable/Product. Closed the serialization half of the defineFields()/defineRules() design gap (no cms-6 core hook exists for adding keys to User::toArray()/Address::toArray()) with four new opt-in, read-only custom field types using dbType(): null — real custom fields flow through toArray()/GraphQL automatically once attached to a field layout via the CP, verified live end-to-end. Also deleted the two legacy Codeception tests asserting attachBehavior()/getBehavior() semantics no longer possible on the new element classes, and cleaned up dangling IDE-hint-only references to the deleted classes.
Also fixes src/Catalog/Products.php, which was referencing the legacy craft\commerce\Plugin class instead of the new CraftCms\Commerce\Plugin.
CKEditor's own field class is still Yii2-based (not yet ported to Craft 6), so the listener still registers via the legacy yii\base\Event::on() mechanism -- there's no Laravel event to hook yet. Redactor isn't supported under Craft 6, so its registration is deleted outright rather than ported.
src/ previously had a three-way Records/Models/Data split per domain folder, with Models/ being an inconsistent mix of Eloquent persistence models and plain Yii2-style data/config objects. This collapses it to a consistent two-namespace convention: Models/ is Eloquent only (extends BaseModel), Data/ is everything else (extends Component), matching the existing Catalog/ProductType/Data and Order/LineItem/Data precedent. Records/ is gone. Also moves Tax\Models\EuVatIdValidator to Tax\VatValidator\Eu, pulling it out of this convention since it's a stateless validator, not a data object. Pure rename, no behavior changes, no BC shims (CraftCms\Commerce\* is still unreleased/alpha).
Ports craft\commerce\Plugin::_registerProjectConfigEventListeners() to CraftCms\Commerce\Plugin::registerProjectConfigEventListeners(), registering against app(CraftCms\Cms\ProjectConfig\ProjectConfig::class) instead of the legacy Craft::$app->getProjectConfig(). The new ProjectConfig::onAdd()/onUpdate()/onRemove() invoke handlers with ItemAdded/ItemUpdated/ItemRemoved (subclasses of the new CraftCms\Cms\ProjectConfig\Events\ConfigEvent), not the legacy craft\events\ConfigEvent, so every handler method this wires up had its type-hint updated to match. The legacy src-yii2/services/* shims keep accepting the legacy ConfigEvent on their own signatures for third-party BC, adapting it into the new type before delegating. Also modernized ProductTypes::pruneDeletedSite() to listen for the Laravel SiteDeleted event instead of the legacy craft\services\Sites::EVENT_AFTER_DELETE_SITE, and the project-config rebuild listener now uses the Laravel ProjectConfigRebuilt event. The Emails::EVENT_AFTER_DELETE_EMAIL listener stays on the legacy yii\base\Event::on() mechanism for now, since that event's firing hasn't been bridged to Laravel yet (see the TODO in Emails::handleDeletedEmail()).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Remaining:
src-yii2/→src/MigrationHigh-level checklist of what's left to finish migrating Commerce from the legacy Yii2 codebase (
src-yii2/,craft\commerce\*) to the new Laravel codebase (src/,CraftCms\Commerce\*).Most of
src-yii2/is already migrated (thinclass_alias()/delegation/extendswrappers). What's below is what's still a full legacy implementation with nosrc/equivalent, grouped by domain. Templates and JS/Vue/SCSS assets are each tracked as a single task, not itemized.TODO
Plugin.php bootstrap migration
Stores::afterDeleteCraftSiteHandler()null-pointer on single-store installs when reassigning primary storeProductTypes::getViewableProductTypeIds()unguarded$user->can(...)call when no authenticated userValidationRulesResolving(CraftCms\Cms\Validation\Events\ValidationRulesResolving) now exists in cms-6, confirming the rules half of this gap is solvable — but traced the actual save pipeline (ElementRequest's permissive wildcard rules +Validates::setAttributes()'s unconditional assignment + the already-wiredCustomers::afterSave{User,Address}Handler()listeners) and confirmedisPrimaryBilling/isPrimaryShipping/primaryBillingAddressId/primaryShippingAddressIdalready round-trip correctly today with zero dependency onRuleset/AddressRules/UserRulesvalidation. No listener added — would be speculative code with no bug it fixes.cms-6core change: added opt-in, read-onlydbType(): nullcustom field types (IsPrimaryBillingField/IsPrimaryShippingField/PrimaryBillingAddressIdField/PrimaryShippingAddressIdField) that admins can attach to theAddress/Userfield layout — real custom fields flow throughtoArray()/GraphQL automatically via the standard field-layout pipeline, no event hook needed. Verified live: attached, saved, confirmed the value round-trips throughtoArray(), then cleaned up the test field.Condition-rule / query-builder system (biggest remaining chunk)
Craft's condition-builder system has no
src/equivalent yet beyond CatalogPricing's own rules. Blocks several other items below (Discounts, Sales, Zones, Gateways, GQL resolvers, the corresponding Pest tests).cms-6provides for its own elements)elements/conditions/orders/*)elements/conditions/products/*)elements/conditions/variants/*)elements/conditions/purchasables/*, excluding the already-migrated CatalogPricing ones)elements/conditions/addresses/*)elements/conditions/customers/*)elements/conditions/users/*)elements/conditions/transfers/TransferCondition.php)Gateways
Dummy/Manual/MissingGatewaygateway driver implementations tosrc/Payment/Gateway/(onlyResponses/,Records/,Contracts/exist so far)Base/Gateway.php+Base/GatewayTrait.phpbase classeshelpers/PaymentForm.phpGraphQL
gql/types/,gql/interfaces/,gql/resolvers/,gql/types/input/,gql/types/generators/)gql/queries/*)gql/arguments/elements/{Product,Variant}.php(currently still legacy-only)helpers/Gql.php_registerGqlInterfaces()/_registerGqlQueries()/_registerRelatedToArguments()(schema-registration half) once the above landsTransfers
Transfersdomain tosrc/—services/Transfers.php,elements/Transfer.php,elements/db/TransferQuery.php,fieldlayoutelements/TransferManagementField.php(currently onlyTransferDetailmodel is migrated)Element actions & field-layout elements
CopyLoadCartUrl,CreateDiscount,CreateSale,DownloadOrderPdfAction,SetDefaultVariant,UpdateOrderStatusProductTitleField,VariantTitleField,VariantsField,UserAddressSettings,TransferManagementField, and thePurchasable*Fieldclasses (SKU, price, stock, weight, dimensions, allowed qty, available-for-purchase, free-shipping, promotable)fields/Products.phpandfields/Variants.php(custom field types)linktypes/Product.phpcan be deleted now thatsrc/Catalog/LinkTypes/ProductLinkType.phpcovers itBehaviors (mostly dead code — needs cleanup, not porting)
Per Group 7 findings:
Site/User/Addressno longer supportattachBehavior(), so these are already non-functional except where Yii2 classes are genuinely still Yii2 (CraftVariable).CustomerBehavior/CustomerAddressBehavior/StoreBehavior— confirmed nothing external references them beyond IDE-hint-only docblocks (CommerceCpAsset.php,tests-yii2/unit/controllers/CartTest.php, both cleaned up), deleted. Also deleted the two legacy Codeception tests (CustomerBehaviorTest.php/CustomerAddressBehaviorTest.php) — they assertedattachBehavior()/getBehavior()semantics impossible on the new element classes, with nothing salvageable to port.CurrencyAttributeBehavior— deleted. Audited every historical consumer:Order/LineItem/Purchasable(coversVariant/Donation)/Productalready had explicitget*AsCurrency()getters. Found and fixed two live regressions along the way ("no live callers insrc/" was the wrong bar — these were public API reachable via the legacy behavior's magic__call, so third-party/template callers matter too):Transactionwas missing 3 getters thatOrdersController.phpand the example templates already call (threw on the order-edit CP screen for any order with a transaction), andCatalogPricingwas missing its getter that Commerce's own shippedprices/_table.twigcalls (threw on Settings → Product Pricing). Also portedShippingMethodOption/OrderAdjustmentgetters for the same third-party BC reasoning even with no in-repo callers.StoreLocationBehavior— deleted. Confirmed dead since before this migration; its functionality was already independently reimplemented (StoreSettings::authorizeStoreLocationView/Edit()+StoreManagementController::save()).ValidateOrganizationTaxIdBehavior— deleted. Superseded by the already-shipped Commerce 5.0 redesign (Order::afterValidate()+ per-storegetValidateOrganizationTaxIdAsVatId()setting).Console
console/controllers/{ExampleTemplatesController,GatewaysController,PricingCatalogController,ResetDataController,TransferCustomerDataController}.phpto Laravel Artisan commands (src/Console/Commands/, following theResaveCommandpattern already used for Groups 6)console/Controller.phpbase once all controllers above are portedHelpers
helpers/Cp.phphelpers/Currency.phphelpers/Locale.php/helpers/Localization.phphelpers/Order.phphelpers/ProductQuery.phphelpers/ProjectConfigData.phphelpers/Purchasable.phpBase classes / traits
Base/InventoryItemTrait.php,Base/InventoryLocationTrait.phpBase/Model.phpBase/Stat.php,Base/StatTrait.php,Base/StatWidgetTrait.phpBase/StoreTrait.phpBase/TaxEngineInterface.php,Base/TaxIdValidatorInterface.php,Base/ZoneInterface.phpTwig / web
web/twig/Extension.php→src/Twig/Extension.php;getGlobals()'scurrentStorenow sourced fromSites::getCurrentSite()->getStore()(theSite::macro('getStore', ...)macro) instead of the removedStoreBehavior;Plugin::boot()'sTwig::registerExtension()call site was already minimal (facade call, one line)web/twig/CraftVariableBehavior.php— deleted. Confirmed via the yii2-adapter (Yii2ServiceProvider::registerCraftVariableCompatibility()) that the legacyCraftVariable::EVENT_INITbridge only forwardsgetComponents(), not attached behaviors, so this never reached the livecraftTwig global under Craft 6 —craft.commerce/orders/products/variantsare already fully served by theNewCraftVariable::macro(...)registrations. Verified live before/after viaTemplateManager::renderString()commercecp,commerceui,inventory,catalogpricing,coupons,transfers, etc.) to the Craft 6 asset pipeline (seedocs/6.x/extend/assets.md)src-yii2/templates/to thesrc/template structure/rendering approachData layer
Install.phpfor fresh installs; the 6 point-release migrations that had been drafted in Laravel format were removed since their schema changes are already baked intoInstall.phpand their data-repair migrations don't apply to a fresh 6.0 schemavalidators/CouponsValidator.php— migrated to a closure rule onCraftCms\Commerce\Promotion\Models\Coupon::getRules()['code'], validated per-coupon inCoupons::saveDiscountCoupons()'s save loop rather than as a single batch check onDiscountTranslations
src-yii2/translations/to top-levellang/(Laravel plugin convention —HasTranslationslooks forlang/besidesrc/before falling back tosrc/translations); message file structure/content unchanged.Craft::t('commerce', ...)→t('...', category: 'commerce')call sites are already converted everywhere insrc/(170 sites); the handful still insrc-yii2/are legitimately deferred until those domains migratePlugin routing/variables cleanup
plugin/LegacyRoutingModule.php(deleted, dead code),plugin/Routes.php(element-edit rules ported to routes/cp.php via EditElementController; barecommerceindex rule deliberately staying until CP templates migrate),plugin/Variables.php(deleted,getDonation()moved toPlugin::getDonation())Queue jobs
queue/jobs/{SendEmail,ResaveProductVariants,CatalogPricing}.phpto native LaravelShouldQueuejobs (src/Email/Jobs/SendEmailJob.php,src/Catalog/Jobs/ResaveProductVariantsJob.php,src/CatalogPricing/Jobs/CatalogPricingJob.php), all 3 call sites now useJob::dispatch()(the idiomaticDispatchable-trait API, which is whatcraft\helpers\Queue::push()/theQueuefacade delegate to internally for object jobs) instead ofcraft\helpers\Queue::push(). No Commerce code referencesLegacyJobWrapperanymore (it's yii2-adapter-owned infra for any remaining legacy jobs elsewhere, not something Commerce itself can delete).Test infrastructure — port
tests-yii2/→ Pest (tests/Feature,tests/Unit)Suggested order per the migration plan, now that
Plugin::boot()/register()fire correctly under Testbench:Currency,Locale,Localization)AverageOrderTotal,NewCustomers,RepeatCustomers,TopCustomers,TopProducts,TopProductTypes,TopPurchasables,TotalOrders,TotalOrdersByCountry,TotalRevenue, baseStat) — low coupling, do nextSale,TaxRate,StoreSettings,LineItemminus its mock dependency) + Adjusters (Discount,Tax)src/service work is activeOrder,Product,Variant,Donation) + Controllers (Cart,Orders,EmailPreview,ShippingRules) — widest surface, do after servicessrc/:DiscountTest,VariantQueryTest, the order/product condition-rule tests,ProductResolverTestsrc/:GatewaysTestGqlCest.php→tests/Feature/Gql/) — lowest priority, needs the full GQL stack wired uptests-yii2/test/{fixtures,mockclasses}/*— portProductFixture/mockPurchasableas needed by the ports aboveDebugPanelHelperTest(feature removed, no replacement)Then
src-yii2/only containsclass_alias()/thin-wrapper files, collapse remaining legacy namespace shims and deprecatecraft\commerce\*per the planCHANGELOG-WIP.mdfor completeness against the final state before release