6.x - #4124
Draft
lukeholder wants to merge 172 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.
CraftCms\Cms\Plugin\Plugins::installPlugin('commerce') calls loadPlugins()
as its own first line, before Commerce has a row in the plugins table yet.
Finding nothing to register, loadPlugins() still permanently sets its
internal pluginsLoaded guard to true. Since Plugins is a container
singleton, every later loadPlugins() call for the rest of the test run
short-circuits immediately -- including the one that would otherwise pick
up Commerce moments later in the same install flow. The net effect: every
boot()-registered feature (GQL argument handlers, widgets, permissions, CP
nav, resave commands, event listeners, Macroable macros) was invisible to
the Pest suite, verified only by hand via tinker against the live app.
Root-caused via a sequence of diagnostic Pest tests (not guesswork):
getPlugin('commerce') returned null despite isPluginInstalled()/
isPluginEnabled() both true; manually calling createPlugin() in isolation
worked fine, ruling out the class/manifest as the problem.
Fix: forget the Plugins singleton and reload it right after installing
Commerce in tests/TestCase.php, so the next resolution re-scans the
now-populated plugins table and actually registers craft\commerce\Plugin
as a Laravel service provider. Same fix shape core's own
CraftCms\Cms\Plugin\Testing\InstallsPlugin trait already applies.
Added tests/Feature/PluginBootTest.php as durable regression coverage,
confirming a Group-1 boot() registration and the Group-7 Site::getStore()
macro (both method-call and magic-property syntax) now resolve correctly.
52/52 tests passing (49 existing + 3 new).
…d along the way
Inventory CP:
- selectableSites() needs an array, not a Collection
- editLocationLevels() return type didn't cover the CpScreenResponse case
- redirect to the default inventory location was missing the CP trigger prefix
Install migration:
- port a real down() (drop tables, field layouts, project config) — it was
previously just a stub, so uninstall silently left the plugin's data behind
- fix an FK identifier over MySQL's 64-char limit
Ported all tests-yii2/unit/stats/*, tests-yii2/unit/helpers/{Currency,Locale,
Localization}Test.php to Pest, and removed the now-redundant legacy DebugPanel
helper test (the DebugPanel feature itself was already removed). Added
tests/Support/OrdersFixture.php to build the order/product/customer graph the
stats tests assert against.
Real bugs surfaced while building out the stats fixture (all pre-existing,
not test-only):
- Variant::getSnapshot() included a `ruleset` validation object that
circularly referenced its own product, crashing JSON encoding on any
order line item
- LineItems.php was missing a use import for LineItemStatuses
- LineItemStatuses + 5 sibling services fired legacy Yii2 events
unconditionally, crashing the first time they actually ran
- 9 legacy condition classes narrowed defineRules() visibility against a
now-public parent, a PHP fatal
- Order::afterSave() didn't convert an already-set dateOrdered to UTC before
storage, breaking timezone-dependent date filtering
- TopPurchasables ordered by an ambiguous `sku` column
Plus SQLite compatibility for the test suite (Stat.php chart queries,
CatalogPricing's UUID/NOW() raw SQL — now extracted into src/Helpers/Sql.php),
and two test-harness gaps (missing auth.guards.craft config, Solo edition's
1-user cap blocking fixture users).
… directly instead of the shared craftcms/.github workflow Track 6.x instead of 5.x, target PHP 8.5, and split tests into Unit/Arch/Feature jobs via a shared run-tests composite action.
Ports 5.x bugfixes (up to 5.7.2) into the Laravel-migrated 6.x code: - Transfers field layout save/delete used Order::class instead of Transfer::class (src-yii2/services/Transfers.php) - PDF/cart load URLs generated from console requests returned blank (src/Pdf/Pdfs.php, src/Order/Carts.php) - Completed orders could have their recalculation mode reset to "all" (src/Order/Elements/Order.php) - Inactive carts' searchindex rows weren't purged due to delete ordering (src/Order/Carts.php) - Inventory location IDs per store weren't memoized (src/Inventory/InventoryLocations.php) New 5.x Codeception unit tests (ShippingTest, OrderRecalculationTest) added as reference material under tests-yii2/unit/, matching how the rest of the retired Yii2/Codeception suite is archived on 6.x.
CatalogPricing.php, Customers.php, Emails.php, and Carts.php referenced classes under the wrong namespace with no matching use import (CatalogPricingRule, Carts, Pdfs, PaymentCurrencies) — real bugs that would fatal at runtime if the code paths executed. ProductQuery::cleanseQueryCriteria() gated its criteria-sanitizing on `Craft::$app->controller instanceof ElementIndexesController` using Yii2 controller classes that no longer exist anywhere in Craft 6. Migrated the check to Laravel routing (request()->route()->getControllerClass()) against the new ElementIndexController/SearchController classes, matching the pattern already used in cms-6's EnsureInstalled middleware.
…Line) These no longer suppressed any actual error - PHPStan flags a bare @phpstan-ignore-next-line as unmatched once the underlying error it was written for has been fixed elsewhere during the migration. Verified each removal leaves zero errors on that line before committing; total PHPStan errors 1306 -> 1078.
…igrations/; remove dead legacy Install migration Plugin::getMigrationsPath() (yii2-adapter) resolves a plugin's migrations directory to dirname(getBasePath())/database/migrations when that directory exists - src-yii2/migrations/ was never in the scan path. These 6 files were already rewritten as genuine Laravel migrations (see b1b20f8) but landed in the wrong directory, so they've never actually run against any 6.x install: product type permission renaming, the commerce_catalogpricing_queue table, orders.customerDeleted, the subscriptions userId FK cascade fix, ordernotices.noticeType, and the allVariants->variants changedattributes fix. Verified via `php craft migrate/all --track=plugin:commerce --pretend` that all 6 are now discovered and their SQL is valid against the current schema. Also removes src-yii2/migrations/Install.php: fully superseded by database/migrations/Install.php (verified matching schema for producttypes), and per docs/extend/migrations.md, once a plugin extends CraftCms\Cms\Plugin\Plugin, only native Laravel migrations run - the old Yii2-style Install migration was already unreachable dead code.
… legacy Order alias
craft\commerce\base\Gateway implements GatewayInterface, which is itself a
class_alias to the new CraftCms\Commerce\Payment\Gateway\Contracts\GatewayInterface.
PHP's LSP compatibility check for that implements clause needs to resolve
Gateway::availableForUseWithOrder()'s craft\commerce\elements\Order parameter
type - which is itself only a class_alias to the new Order class, created
lazily on first reference.
If nothing has touched craft\commerce\elements\Order yet, resolving it happens
reentrantly, mid-compatibility-check, while PHP is already inside the include()
for Gateway.php - which reliably fails with "class ... is not available" rather
than actually resolving it. Reproduced independent of PHPStan with a plain
`class_exists('craft\commerce\base\Gateway')` after only requiring the
autoloader.
Force-resolving the Order alias as a plain top-level statement, before the
class Gateway declaration is reached, avoids the reentrant path entirely.
Commerce's Model/Record classes (ProductType, Order, LineItem, Store, OrderQuery, Email, Pdf, Transaction, Purchasable, etc.) rely on Eloquent's magic __get/__set for every column, which plain phpstan/phpstan has no way to type. That's the source of the large majority of PHPStan's property.notFound/method.notFound/staticMethod.notFound noise. Mirrors cms-6's phpstan.neon: includes larastan/larastan + nesbot/carbon's extension, and points databaseMigrationsPath at database/migrations (Install.php plus the 6 migrations moved there in the previous commit) so Larastan can infer real column types instead of requiring @Property annotations on every model. property.notFound/method.notFound/staticMethod.notFound: 511+295+100 -> 95+120+15. Total PHPStan errors: 1078 -> 429.
…rastan Same cleanup as before, for @phpstan-ignore-next-line comments that only became unmatched once Larastan resolved the underlying property/method errors they were suppressing. Total PHPStan errors: 429 -> 422.
Real fixes: - Log::error($e) passed a raw exception where Logger::error() needs a string/Arrayable/Stringable message - switched to Log::error($e->getMessage(), ['exception' => $e]) - Order\Models\Order was missing datetime casts for datePaid, dateFirstPaid, dateAuthorized, dateCreated, dateUpdated, so assigning the element's real DateTime values onto the persistence model was statically (and would eventually be a real) type mismatch; added the casts and convert via Carbon::instance() at the assignment sites - getLineItems() ran array_filter() over a strictly LineItem[]-typed array, which can never contain falsy values - dead code, removed - clearNotices()/_filterNotices() had redundant "other side is not null" checks in an exhaustive if/elseif chain, provably always true given the preceding branches - simplified Docblock/type annotations (no behavior change): - Added missing @property-read entries for the *AsCurrency accessors on Order and LineItem, and @Property for OrderNotice::$noticeType (now backed by the noticeType column from the migration moved in a previous commit) - Added @var AddressElement assertions where Elements::duplicateElement() is called through the facade, which loses its generic <T> narrowing through the @method docblock and returns plain ElementInterface Suppressions, for genuine PHPStan/Larastan blind spots (not real bugs): - User::getPrimary{Shipping,Billing}Address()/getPrimaryPaymentSource() are added via the legacy CustomerBehavior at runtime, invisible to static analysis - Several Order::trigger()/LineItem trigger() calls pass new-namespace event objects into the still-Yii2 trigger() signature - matches the "TODO: migrate event firing to Laravel" pattern already used elsewhere - Two nullsafe.neverNull findings were false positives (getCustomer() and firstWhere() are both genuinely nullable) - changing to non-null access would introduce a real null-pointer bug - Gateway/GatewayInterface class_alias-chain blind spot (same one already documented in GatewayTypes.php) hit two more call sites - Conditions::createCondition()'s InvalidArgumentException is real (verified in cms-6) but invisible to PHPStan through the facade's __callStatic dispatch - catch is not dead - CraftCms\RulesetValidation\Ruleset's template T isn't covariant, so PHPStan can't verify any ElementRules subclass against the #[Ruleset] attribute - same limitation cms-6's own AssetRules/EntryRules/UserRules have no workaround for either Total PHPStan errors: 422 -> 360.
…l addError() runtime bug phpstan.neon was missing the whole stubFiles list that cms-6's own config includes (yii/base/Component, yii/validators/Validator, yii/db/*, etc.) - these carry the accurate typed signatures for legacy Yii2 APIs (e.g. Validator::addError() accepting craft\base\ModelInterface, not just the untyped vendor source). Total PHPStan errors: 360 -> 259. Purchasable.php: - getLineItemRules()'s inline validators called $validator->addError($lineItem, $attribute, $message), routing through yii\validators\Validator::addError() which internally calls $lineItem->addError($attribute, $message) - but LineItem is a new Component/Validates class with no such method, only errors(): MessageBag. This was a real bug: any of these six validators actually firing (out of stock, invalid purchasable, qty limits, etc.) would fatal at runtime with "Call to undefined method LineItem::addError()". Switched to $lineItem->errors()->add($attribute, $message), matching the pattern already used throughout Order.php/Product.php. - Localization::normalizeNumber() was called via CraftCms\Commerce\Helpers\Localization, which only has normalizePercentage() - normalizeNumber() is a Craft core helper (craft\helpers\Localization). Fixed the import. - Added @property/@property-read docblock for storeId, basePrice, basePromotionalPrice, sku, taxCategoryId, shippingCategoryId, and the *AsCurrency accessors. - getSku() had a dead `?? ''` fallback on a non-nullable string property. Also removed two @phpstan-ignore-next-line comments (on Order's and Purchasable's #[Ruleset(...)] attributes) that the stub fix made stale. Total PHPStan errors: 259 -> 251.
…ressions Real bugs (all would fatal at runtime on the affected code paths): - loginHandler() referenced User::IMPERSONATE_KEY, a constant that doesn't exist anywhere in the codebase - Craft 6's impersonation session state moved to a dedicated CraftCms\Cms\Auth\Impersonation service. Switched to app(Impersonation::class)->isImpersonating(). This ran on every user login. - activateUserFromOrder() called $user->setScenario(Element::SCENARIO_ESSENTIALS) using scenario constants that live on ElementRules, not Element (and via craft\base\Element, itself just a class_alias to the new Element) - Craft 6's scenario API is $element->ruleset->useScenario(...), not setScenario(). This ran on every guest checkout with "create an account" enabled. - The same method used Event::once(...), which has never existed on yii\base\Event (only on(), off(), trigger(), etc.) - replaced with the standard self-removing on()/off() handler pattern. Dead code removed: two property_exists($user, 'affiliatedSiteId') guards - affiliatedSiteId is a real, always-present property on Craft 6's User now. Suppressions, for the same CustomerBehavior/CustomerAddressBehavior runtime-attached-method blind spot already documented in Order.php - not real bugs, PHPStan just can't see methods added via legacy Yii2 behaviors. Also suppressed one nullsafe.neverNull false positive (getBillingAddress()/ getShippingAddress() are genuinely nullable, matching the pattern already found in Order.php). Total PHPStan errors: 251 -> 229.
Corrected 13 @PHPStan-Ignore comments (in Order.php and Customers.php, added in the previous two commits) that incorrectly attributed User/Address methods to "the legacy CustomerBehavior/CustomerAddressBehavior, attached at runtime". Plugin.php's registerBehaviorMacros() docblock clarifies those behaviors "no longer attach to anything" post-migration - Site/User/Address extend CraftCms\Cms\Component\Component, not yii\base\Component, so attachBehavior() doesn't exist on them at all. The real mechanism is Macroable macros registered in Plugin::registerCustomerMacros()/registerCustomerAddressMacros(). Fixed the comment text to name the real mechanism instead. OrdersController.php fixes: - currentUser()?->id read a nonexistent property on the CraftUser contract (only getCraftUserId() exists) - silently wrote null into $movement->userId on every inventory fulfillment movement instead of the actual current user. - getPaymentModal() never null-checked $order after getOrderById(), then called methods on it unconditionally - added an abort_unless() guard matching the rest of the controller's pattern. - $child->order->updateOrderPaidInformation() after capture/refund could hit a null Order (Transaction::$order is genuinely nullable) - switched to nullsafe. - Simplified two provably-dead conditions (getFieldLayout() is overridden non-nullable on Order; $qty's `?? 1` already eliminates null) and made two abort_unless() truthy-checks explicit. Also added the missing Transaction::$order @property-read docblock and Order::$outstandingBalanceAsCurrency, and suppressed the remaining Site::getStore()/Gateway class_alias-chain blind spots using the corrected macro explanation. Total PHPStan errors: 229 -> 209.
…ives Real bug: defineTableAttributes() merged in 'product'/'isDefault'/'promotable' as flat translated strings instead of ['label' => string] arrays - every other definer in this file (defineCardAttributes()) and the base DisplayedInIndex::defineTableAttributes() implementation use the wrapped shape. Fixed to match. Suppressed three more nullsafe.neverNull/nullCoalesce.expr findings that are the same class of false positive already seen in Order.php/Customers.php: getOwner() is declared ?ElementInterface and variantTitleTranslationMethod is an uncast free-form DB string column, so tryFrom() genuinely can return null. Discovered along the way that @phpstan-ignore-next-line only covers errors reported on the literal next physical line, not later lines within the same multi-line statement - switched those specific cases to a trailing @phpstan-ignore-line comment on the actual offending line instead. One nullsafe chain (getProductTypeHandle()) was simplified rather than suppressed, since Product::getType(): ProductType is a verified non-nullable override. Total PHPStan errors: 209 -> 199.
normalizeLineItemPurchasableAvailability() cast $lineItem->getPurchasable() (genuinely ?PurchasableInterface) to a non-nullable @var PurchasableInterface, masking the null check right below it from static analysis. Worse, the elseif branch called hasInventory()/getIsOutOfStockPurchasingAllowed()/ getStock()/inventoryTracked, all specific to the concrete Purchasable element class, not part of PurchasableInterface's contract - a third-party PurchasableInterface implementation that doesn't extend Purchasable would have fataled here. Fixed the @var annotation to be nullable and added an instanceof Purchasable guard before the stock-specific calls. Also fixed two missing `use function CraftCms\Cms\t;` imports (function t() not found).
…e docblocks - Added @property-read $price/$promotionalPrice/$salePrice to Purchasable (fixes Variant::$price undefined-property error in getDefaultPrice() and covers every other Purchasable subclass too). - Same tryFrom()-on-uncast-string-column suppressions as Variant.php, for productTitleTranslationMethod/slugTranslationMethod. - updateTitle() had a defensive "check for null just in case the value comes back as 1, 0, true or false" - hasProductTitleField is a NOT NULL boolean column properly cast by Larastan; the null check was dead.
…findings sendEmail() had a leftover "make sure date vars are in the correct format" loop converting Order's dateOrdered/datePaid/dateFirstPaid to DateTime if they weren't already - a legacy carry-over from when these were raw DB strings. They're now plain `?DateTime` typed properties on the Order element, so the loop's condition (instanceof DateTime check combined with a truthy check) was provably always false - dead code, removed. Suppressed one more TODO-marked trigger() event-bridging call and one nullsafe.neverNull finding ($renderSite is genuinely nullable, from a ternary that explicitly assigns null in its else branch).
…y suppressions Real bug: the multi-currency conversion branch in createTransaction() passed whole PaymentCurrency model objects into getTeller()/convertAmount(), both of which are typed Money\Currency|string - under strict_types this would throw a TypeError, not silently coerce. Fixed to pass the ->iso string, matching how the rest of the method already dereferences ->iso. Everything else in both files is the same legacy craft\commerce\base\Gateway class_alias-chain blind spot already established (paymentType, completePurchase/completeAuthorize/capture/supportsRefund/supportsPartialRefund/ supportsCapture/refund, and one @var Gateway downcast PHPStan can't verify), plus one more TODO-marked trigger() event-bridging call, plus a craft\commerce\models\Transaction class_alias PHPStan can't trace either.
…block conflict The macro closures in registerCustomerMacros()/registerCustomerAddressMacros() call other macros registered on the same class (e.g. getPrimaryBillingAddress() calling getPrimaryBillingAddressId()) - PHPStan can't trace Macroable dispatch even within its own registration site. The three orderBy() "arguments.count" errors are a genuine cms-6 issue: ElementQuery's class docblock has `@method static orderBy($column)` (1 param) which conflicts with its own real `orderBy($column, $direction = 'asc')` method (2 params) - PHPStan prioritizes the stale docblock tag. Not something to fix here since ElementQuery is owned by cms-6; suppressed with an explanation instead.
…me() false-path suppressions
Real fix: $number = str_replace(...) then $number-- decremented a plain
string rather than a numeric type - cast to (int) first.
Every DateTimeHelper::toDateTime()/strtotime() result flowing through this
file only fails for an unparseable input, and every input here is either a
hardcoded relative-date string ('first day of this month', etc.) or an
already-validated Y-m-d string - the DateTimeInterface|false union is
real per the signature but practically unreachable here. Suppressed rather
than adding defensive checks for a case that can't happen.
groupByRaw()/orderByRaw() SQL fragments are built entirely from server-side
driver/timezone detection in getChartQueryOptionsByInterval(), never from
user input, so PHPStan's literal-string requirement (an anti-SQL-injection
check) doesn't apply here either - suppressed with the same reasoning.
…ro/facade suppressions Real bug: `use Illuminate\Http\Response as JsonResponseAlias;` was importing the wrong class - JsonResponse does NOT extend Response (they're siblings, both extending different Symfony HttpFoundation base classes separately), so filter()/prices() were declared to return the aliased Response while actually returning response()->json(...) (a real JsonResponse). Fixed the import and both return types to the real Illuminate\Http\JsonResponse. Everything else is the established Site::getStore() macro suppression pattern, plus a @var CatalogPricingCondition assertion where Conditions::createCondition() loses its narrow type through the facade's __callStatic dispatch (same category as the Elements::duplicateElement() facade-narrowing issue fixed earlier in Order.php).
…er-input date bug plus established patterns
Real bug: ProductsController::create() assigned
DateTimeHelper::toDateTime($request->input('postDate'/'expiryDate')) directly
to Product's ?DateTime properties - unlike the Stat.php/Order.php cases fixed
earlier, these inputs are genuinely user-controllable (CP form fields), so a
malformed date string would return false and, under strict_types, throw a
TypeError. Added a real fallback (now() / null) and converted to a concrete
DateTime instance.
Donation.php had the same $validator->addError($lineItem, ...) bug already
found and fixed in Purchasable.php (LineItem has no addError() method) -
switched to $lineItem->errors()->add(). Also added the missing
Donation model datetime casts (same gap as Order's model, fixed earlier)
and converted afterSave()'s date assignments through Carbon::instance().
LineItem was missing @Property float $price (writable, has get/setPrice())
entirely - added.
Everything else is abort_unless() truthy-to-explicit-null-check cleanup and
the established Gateway class_alias-chain suppressions.
…suppressions Real bug (severe): Helpers/Cp.php, Currency.php, and Purchasable.php all imported CraftCms\Cms\Cp\Cp - a tiny unrelated class with only config()/ vite() static methods. It has nothing to do with form-field HTML. Every call in these three files (fieldHtml, textHtml, moneyInputHtml, lightswitchHtml, renderTemplate) would fatal with "undefined static method" - this is the entire Commerce CP UI for tax zones, tax categories, shipping categories, inventory locations, money inputs, and price tables. Fixed the imports to the legacy craft\helpers\Cp bridge, which has all of these methods under the same names. Real bug: request()->craftUser()?->id / currentUser()?->id read a nonexistent property on the CraftUser contract (only getCraftUserId() exists) in Inventory.php, InventoryLocations.php, and OrderStatusesController.php - same category of bug already found and fixed in OrdersController.php, silently writing null instead of the real user ID. Real fix: LineItemStatuses::handleArchivedLineItemStatus() formatted a date to a DB string via CraftDb::prepareDateForDb() and assigned it to a Carbon-cast property - simplified to Carbon::now() directly, avoiding an unnecessary format/reparse round-trip. Real fix: Inventory::reduceInventoryForOrder() (executeInventoryMovements loop) called hasInventory()/getInventoryLevels(), both specific to the concrete Purchasable class, on a PurchasableInterface-typed variable with no instanceof guard - same category of bug already fixed in Helpers/Order.php and Donation.php. Everything else is the established Site::getStore() macro suppression, TODO-marked trigger() event-bridging suppressions, OrderAdjustment missing its $sourceSnapshot docblock, and two provably-dead null checks (Discount::$baseDiscount is a non-nullable float, InventoryMovementCollection already yields the concrete InventoryMovement type without needing a downcast @var).
Real bug: CatalogPricingRulesController's edit screen called ->tabs([...]) with a plain numeric-indexed list of tab configs, but CpScreenResponse::tabs() requires string keys matching each tab's container id (confirmed against every other controller's ->tabs() call in this codebase, e.g. ProductTypesController) - this would have rendered broken/non-functional tabs in the CP. Fixed to use 'rule'/'conditions'/'actions' keys. Same Carbon::instance()/DateTime::createFromInterface() conversions as the Order/Donation fixes earlier, now for CatalogPricingRule, Discount, Sale, and Gateway records/models, both at the record-save sites (domain model's plain ?DateTime -> Carbon-cast record property) and the controller user-input sites (DateTimeHelper::toDateTime() can genuinely return false for a malformed date). Two LineItemStatuses/Gateways-style CraftDb::prepareDateForDb(new DateTime()) "archive now" assignments simplified to Carbon::now() directly. Added the missing CatalogPricingRule::$applyAmountAsPercent/$applyAmountAsFlat docblock. Simplified one more redundant elseif branch in Discounts.php (same provably-redundant-given-preceding-if pattern fixed in Order.php earlier), and removed one incorrect Gateway suppression that turned out to be unnecessary once the return type was already narrow enough.
…other real bugs Real bug (severe): inventoryLevelsTableData() built its query entirely with Yii2 query-builder syntax (andWhere() with nested condition arrays like ['not', [...]] and ['or', [...], [...]], [[bracket]]-quoted columns, addGroupBy(), addOrderBy()) against a genuine Laravel Illuminate\Database\ Query\Builder returned by Inventory::getInventoryLevelQuery(). None of that syntax exists on Laravel's builder - this is the main data-table endpoint for the Inventory CP screen, so it would have fatally errored on every page load. Converted to real Laravel builder calls (where(), a where() closure for the OR/LIKE search, whereNotNull(), groupBy(), orderBy(), a proper leftJoin() with column args), replaced ->all() with ->get() and the limit(null)/offset(null) reset trick with getCountForPagination(), and fixed the resulting Collection-vs-array usage (array_column -> pluck/filter/unique). Also fixed two CpModalResponse-returning modal actions (editUpdateLevelsModal/editMovementModal) that had an early "live preview" return path returning a real JsonResponse but were typed to return only CpModalResponse/Response - widened to CpModalResponse|JsonResponse. InventoryLocationsController: setAttributes() called with a second `false` "safeOnly" argument that no longer exists on the new Validatable contract - confirmed the new setAttributes() always behaves like the old safeOnly=false mode, so dropping the arg preserves behavior. Also fixed a facade-narrowing Address lookup to handle a genuinely-possible null (stale/deleted address id) instead of asserting non-null. abort_unless()/abort_if() truthy-check cleanup across CartController, DownloadsController, ProductTypesController, and TransfersController (array|string|null / object|null params made explicit), plus two provably-dead checks in CartController (LineItem::$qty is a non-nullable int; returnCart() never actually returns null).
…ro, and legacy-property errors Catalog/CatalogPricing: - CatalogPricing model: reference real CatalogPricingRule class instead of legacy craft\commerce\models\CatalogPricingRule - Products/Variants: assert narrow Product/Variant type after Elements facade's getElementById() call, which loses generic type via __callStatic - VariantQuery: suppress owner()/primaryOwner() param-type mismatch against NestedElementQueryInterface's stricter signature - CatalogPricing::getCatalogPricesPageInfo(): use getCountForPagination() instead of get()->count() to avoid fetching unnecessary rows - Add @var assertions for Conditions::createCondition() facade calls Controllers: - ProductQuery::cleanseQueryCriteria(): drop unnecessary nullsafe on request(), which always resolves the bound Request singleton - PaymentsController: suppress legacy GatewayTrait getIsFrontendEnabled()/ $handle access via the class_alias chain, drop stray setAttributes() 2nd arg, initialize $error before conditional assignment, fix abort_unless() bool coercion - EmailsController: use '' instead of null as an array key (PHP already coerces null keys to ''), call Email's setTo()/setBcc()/setCc() setters instead of nonexistent magic properties - StoreManagementController: resolve Site::getStore() macro result once into a typed local instead of repeating nullable/macro type assertions - TaxRatesController: call TaxRate's real hasTaxIdValidators()/ getIsEverywhere()/getTaxZone()/getTaxCategory() methods instead of nonexistent magic properties - UserOrdersController/WebhooksController: suppress Macroable macro and class_alias chain false positives - Order/Carts: drop invalid `false` default passed to Request::cookie() (expects array|string|null), compare against null instead
…— 0 errors project-wide - LineItemStatus::get(): widen return type from ?static to ?self to match what LineItemStatuses::getLineItemStatusById() actually returns - OrderQuery: remove dead is_array() check (containsPurchasables shape is already typed as always-array), suppress afterHydrate()'s intentional Collection<Order> narrowing of the interface's Collection<ElementInterface> - Payment/Currencies::getAllCurrencies(): assert Collection<int, Currency> since collect() can't infer element type from ISOCurrencies' plain IteratorAggregate implementation - PaymentSource/PaymentSources: suppress legacy craft\commerce\base\Gateway class_alias chain false positives (assign.propertyType, method.notFound) - Purchasables::updateStoreStockCache(): guard with instanceof Purchasable before calling Inventory::getInventoryLevelsForPurchasable(), which only accepts the concrete inventory-trackable element class - Stats/RepeatCustomers: use getCountForPagination() instead of get()->count() to avoid fetching unnecessary rows Full project phpstan analyse now reports 0 errors (362 files).
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 usercms-6core change, not just Commerce): no extension point fordefineFields()/defineRules()equivalent onUser/Addressnow that behaviors are gone —primaryBillingAddressIdetc. can't be validated or serialized. @rias mentioned looking intoValidationRulesResolvingCondition-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— confirm nothing external still references them, then delete (functionality already replaced by macros in Group 7)CurrencyAttributeBehavior— assess whether still needed / has a Laravel-native equivalent (casts?)StoreLocationBehavior— assess and migrate or deleteValidateOrganizationTaxIdBehavior— migrate to the new validation/Ruleset systemConsole
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— port tosrc/, then simplifyPlugin::boot()'sTwig::registerExtension()call siteweb/twig/CraftVariableBehavior.php— stays legacy intentionally (real Yii2CraftVariable, not aliased); revisit only if core changes thatcommercecp,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
database/migrations/(Laravel migration format) alongsideInstall.phpvalidators/CouponsValidator.php— migrate to the new Ruleset/validation systemTranslations
docs/6.x/extend/translation.md), updatingCraft::t('commerce', ...)call sites tot('...', category: 'commerce')as each domain migratesPlugin routing/variables cleanup
plugin/LegacyRoutingModule.php,plugin/Routes.php(remaining rules deliberately staying — re-check only if core routing changes),plugin/Variables.phpQueue jobs
queue/jobs/{SendEmail,ResaveProductVariants,CatalogPricing}.phpto native LaravelShouldQueuejobs, then drop theLegacyJobWrappershim and switchcraft\helpers\Queue::push()call sites to theIlluminate\Support\Facades\QueuefacadeTest 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