Repository navigation
6.x - #4124
Draft
lukeholder wants to merge 463 commits into
Draft
6.x#4124lukeholder wants to merge 463 commits into
lukeholder wants to merge 463 commits into
Conversation
lukeholder
marked this pull request as draft
September 23, 2025 07:42
lukeholder
added a commit
that referenced
this pull request
Aug 20, 2026
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.
Fixes the "UserQuery::EVENT_AFTER_POPULATE_ELEMENTS was removed in Craft 6" TODO in craft\commerce\Plugin::_registerCraftEventListeners(). The legacy event doesn't exist in Craft 6, and it turns out ElementPopulating/ElementPopulated aren't it either -- those are GraphQL-mutation-specific. The real equivalent is CraftCms\Cms\Element\Queries\Events\ElementsHydrated, fired from HydratesElements::hydrate() for every element type's query (not scoped to UserQuery like the old event was), so the listener filters for User instances itself. Ports the bulk-populate logic for primaryBillingAddressId/ primaryShippingAddressId to CraftCms\Commerce\Plugin, pre-warming ObjectState for a batch of hydrated Users so the getter macros (registerCustomerMacros()) don't each run their own per-user query.
Moves the customer-attach bulk-populate logic out of an anonymous closure in Plugin::registerCraftEventListeners() into a proper named method, Customers::elementsHydratedHandler(), matching the existing *Handler naming convention on that service (loginHandler, afterSaveUserHandler, etc). Registers it declaratively via a new Plugin::$events array instead (CraftCms\Cms\Plugin\Concerns\HasListeners, already inherited by every plugin but not yet used by Commerce), using Laravel's 'ClassName@methodName' listener-string syntax so the handler doesn't need to be named handle() or live in a dedicated listener class.
Swaps the ElementsHydrated => Customers::class.'@elementsHydratedHandler' entry for a real listener class, CraftCms\Commerce\Customer\Listeners\ElementsHydratedListener, matching the pattern cms-6's own HasListenersTest demonstrates as the intended convention. Plugin::$events now registers a bare class-string, so the phpstan-ignore needed for the magic string is no longer required.
Moves craft\commerce\Plugin::_registerPoweredByHeader() to CraftCms\Commerce\Http\Middleware\PoweredByHeader, pushed onto the craft middleware group in Plugin::boot() -- matching how core's own CraftCms\Cms\Http\Middleware\SetHeaders already handles the "Craft CMS" half of this header via the same group.
Moves schemaVersion/minVersionRequired/hasCpSettings/ hasReadOnlyCpSettings directly onto the new Plugin class, and relocates HasCommerceEditions to src/Plugin/Concerns/ (matching HasPermissions/HasServices) with typed constants and declare(strict_types=1). Fixes a real bug: $minCmsEdition was Edition::Solo (the HasEditions base-trait default) instead of Edition::Pro. The legacy craft\commerce\Plugin's own CmsEdition::Pro override masked this everywhere it's actually bootstrapped -- except the Pest suite, which boots CraftCms\Commerce\Plugin directly and was silently running with the wrong minimum edition. Removed the now-redundant declarations (plus an orphaned docblock) from craft\commerce\Plugin.
Converts craft\commerce\Plugin into a pure class_alias for CraftCms\Commerce\Plugin, matching every other fully-migrated legacy class in this migration (e.g. Transfers). Everything it still did gets ported, dropped as redundant, or dropped as dead/broken: - Ported getSettingsResponse()/getReadOnlySettingsResponse()/ createSettingsModel() onto the new Plugin. Added a getSettings(): ?Settings override narrowing HasSettings' base ?Validatable return type, fixing 25 phpstan errors that only surfaced once the settings chain was fully wired up. - Ported the relatedToProducts/relatedToVariants GraphQL argument definitions to the modern GqlArgumentsResolving event. - Dropped _registerGqlInterfaces()/_registerGqlQueries() entirely -- fully redundant with the already-shipped $gqlTypes/$gqlQueries arrays. - Ported the FK-restore utility registration and the one remaining legacy CP route registration as-is (both still legacy Event::on(), no Laravel equivalent exists yet for either). - Dropped beforeInstall()'s version-guard checks and the @commercelib alias -- both dead/redundant. - Found and dropped a pre-existing bug rather than porting it: the Purchasable-restore event registration calls a method, beforeRestorePurchasableHandler(), that doesn't exist anywhere in the codebase -- confirmed present in the shipping 5.x branch too. Restoring a trashed purchasable would throw a fatal error if this ever actually fired. Deleted src-yii2/plugin/Routes.php (and the now-empty src-yii2/plugin/ directory) -- fully absorbed into the above. Also split Plugin.php (previously ~990 lines) into two more Concerns traits: HasCommerceMacros (all Macroable macro registration) and HasCommerceEventListeners (all event listener registration, including everything ported above), matching the existing HasPermissions/ HasServices/HasCommerceEditions pattern.
Replaces the imperative Event::listen() calls in registerCraftEventListeners() with 8 dedicated classes under CraftCms\Commerce\Plugin\Listeners\*, registered declaratively via Plugin::$events -- matching the ElementsHydratedListener pattern. $events-based registration runs unconditionally (HasListeners has no gating mechanism), unlike the old method which only ran when $this->isInstalled. Each listener's handle() now starts with its own isInstalled guard to preserve that original protection.
# Conflicts: # src/Http/Controllers/Settings/ProductTypesController.php
Retires the Catalog namespace and redistributes its 48 files: Product/Variant/ProductType and the rest of the genuine product-catalog domain move to a new top-level Product namespace, with Variant nested under Product\Variant (mirroring the existing Order\LineItem nested-child-element precedent) and ProductType nested under Product\ProductType. Five files that were misplaced under Catalog despite being genuinely CatalogPricing-domain (CatalogPricing/CatalogPricingRule data classes and their CatalogPricingRule*Condition* rules) move into the existing CatalogPricing namespace instead. ModifyPurchasablesTableQueryEvent moves to Purchasable\Events. Product doesn't merge into the existing Purchasable namespace, which stays scoped to the abstract/shared-purchasable-behavior layer (base Purchasable element, Donation, generic cross-type conditions) rather than conflating it with the concrete product catalog domain. Since Product/Variant used to share a namespace and could reference each other by bare class name, splitting them into separate namespaces surfaced ~15 files with unqualified cross-references that needed explicit use imports added (e.g. Variant.php referencing bare Product, ProductQuery.php referencing bare VariantQuery) -- caught via phpstan's class.notFound errors, not the reference-rewrite script itself, which only rewrites fully-qualified names. The Catalog namespace never existed in a tagged release (introduced and retired entirely within this unreleased WIP branch), so no class_alias compatibility shims or changelog "moved from" notes are needed -- the changelog documents the final Product/CatalogPricing/ Purchasable locations directly, as if they'd always been there.
Both were the original, untouched Yii2 implementations sitting under src-yii2/base/ -- not class_alias/delegation stubs like the rest of that directory, since traits can't be class_alias'd. Their content is already inlined directly on the new classes (src/Payment/Gateway/ Gateway.php has the same properties/methods; src/Stats/Stat.php has the same properties), and nothing in the codebase actually `use`s either legacy trait anymore, so they were dead code rather than an unported gap. Also updates two stale PaymentsController.php phpstan-ignore comments that attributed the property/method declaration to the now-deleted GatewayTrait -- they're declared directly on CraftCms\Commerce\Payment\Gateway\Gateway.
Five call sites still referenced craft\commerce\* classes even though
their CraftCms\Commerce\* replacements already exist and are used
everywhere else in src/ -- these look like straightforward oversights
from when GQL/enum/element work landed elsewhere, not open porting
gaps:
- Product/Fields/{Products,Variants}.php imported the legacy GQL
arguments/interface/resolver classes instead of
CraftCms\Commerce\Gql\{Arguments,Interfaces,Resolvers}\Elements\*.
- PurchasablePriceField.php's `@var Sale[]` docblock pointed at the
legacy model instead of CraftCms\Commerce\Promotion\Data\Sale,
which is what Sales::getSalesRelatedToPurchasable() actually returns.
- Discounts.php compared against the legacy LineItemType enum under
a self-flagged "TODO: update LineItemType enum reference once
migrated" -- CraftCms\Commerce\Order\LineItem\Enums\LineItemType
already exists.
- UpdateOrderStatus.php called app(craft\commerce\services\OrderStatuses::class)
under a similar stale TODO instead of the fully-ported
CraftCms\Commerce\Order\OrderStatuses.
- OrderStatus::canDelete() queried craft\commerce\elements\Order
instead of the fully-ported CraftCms\Commerce\Order\Elements\Order.
registerVariableMacros(), getDonation(), registerBehaviorMacros(), and get() had multi-line docblocks narrating what they do and why, beyond what the method name/body already make clear -- no behavior change.
Replaces every remaining "TODO: migrate event firing to Laravel once event system is bridged" site (~60 across Carts, Customers, Emails, Inventory, LineItems/LineItem, LineItemStatuses, OrderHistories, OrderStatuses, PaymentCurrencies, Payments, PaymentSources, Pdfs, ProductTypes, Purchasables, ShippingMethods, Stores, Taxes, Transactions, Webhooks, plus the Order/Product/Variant elements' own instance-level events) with real event() dispatch, following the pattern Craft core's own yii2-adapter uses for this exact problem (e.g. craft\services\Volumes::registerEvents()): - src/ code now only calls event($event) -- no more manual hasEventHandlers()/trigger() dual-fire at each call site. - Each legacy craft\commerce\services\* shim gets a new `public static function registerEvents(): void` that listens for the new event class and forwards it to the legacy EVENT_* trigger on the legacy component instance, so real Event::on(...) listeners on the legacy class keep working. - All 19 registerEvents() calls are wired once from Plugin::boot() via a new HasCommerceEventListeners::registerLegacyEventBridges(). - Order/Product/Variant, whose events fire via $this->trigger() directly on the element (bridged per-instance by the yii2-adapter's LegacyBehaviorMixin, not a separate legacy service object), keep the inline dual-fire pattern instead, since there's no separate legacy instance to centralize a bridge onto. Splits several event classes that were previously reused across multiple distinct named events (EmailEvent for all 4 email lifecycle events, PdfEvent/PdfRenderEvent, StoreEvent/DeleteStoreEvent, PaymentSourceEvent, TransactionEvent/RefundTransactionEvent, ProcessPaymentEvent, WebhookEvent, ProductTypeEvent, LineItemEvent) into dedicated subclasses (e.g. EmailSaving/EmailSaved/EmailDeleting/ EmailDeleted) -- required for the new centralized bridge, which listens by class, so a shared class firing for one named event would otherwise also incorrectly re-fire every other legacy event name sharing that class. Matches the one-class-per-event convention craft\services\Volumes's own bridged events already use. Fixes a real, pre-existing bug surfaced by actually exercising these event-firing code paths for the first time (previously hasEventHandlers() almost always returned false since nothing registered a legacy listener, so the trigger() calls were never reached): every Commerce event class constructed at a trigger() call site needed to extend yii\base\Event, since yii\base\Component::trigger(string $name, ?yii\base\Event $event = null) has a hard type check and throws a TypeError for any other object. Commerce's event classes had been plain PHP classes since being ported to src/, so any real legacy Event::on(...) listener would have crashed the save/delete/etc. call that fired it. Verified live: a legacy Event::on() listener on craft\commerce\services\Emails/PaymentCurrencies/ Product\Variant\Elements\Variant now fires correctly with no error.
Documents the event-firing migration and the GatewayTrait/StatTrait removal (Deprecated -> Removed, since they're now deleted rather than kept as a compatibility shim), and trims a handful of unrelated by-domain entries down to the file's usual terse "Removed/Deprecated X. Y should be used instead." shape, cutting rationale clauses that explained implementation mechanics rather than what changed.
Both were the last real unported dependencies left in src-yii2/. VariantCollection becomes CraftCms\Commerce\Product\Variant\Elements\VariantCollection. OrderCustomersDeletionBlocker now extends CraftCms\Cms\Element\DeletionBlockers\BaseDeletionBlocker instead of the legacy Yii2 one, and swaps Craft::t()/craft\helpers\Html::jsWithVars()/ craft\helpers\Cp::elementIndexHtml() for their src/ equivalents. class_alias breadcrumbs left at both old craft\commerce\elements\* locations for BC.
None of these 23 sites needed Yii2's DI container - they were all just instantiating a plain element (Order, Address, OrderNotice) or a CraftCms\Cms\Component\Component-based model with a set of attributes. Elements use new X() + setAttributes(), Component-based models use new X([...]) directly against their existing constructor contract. Also fixes a latent bug in DiscountsController::setCouponsOnDiscount(): it built the Coupon via a double-wrapped ['config' => ['attributes' => [...]]] shape left over from the old Yii2 Model convention, which doesn't match Component's __construct(array $config) contract - the coupon's id/code/uses/maxUses were never actually being set.
- Order::setShippingAddress()/setBillingAddress()/setEstimatedShippingAddress()/
setEstimatedBillingAddress() now apply $address['fields'] via setFieldValues(),
and the now-redundant duplicate calls in CartController are removed.
- Variant regenerates its auto-generated SKU in afterAssignedId() when skuFormat
references {id}, since that placeholder can't resolve until the element has an ID.
- Store::afterSaveCraftSiteHandler() skips creating a site-store mapping while a
project config apply is in progress, deferring to handleChangedSiteStore().
- index.twig's editableProductTypes -> getViewableProductTypes() rename was
already effectively present via equivalent property-style Twig access.
- Ported the two new Codeception reference tests covering the SKU and Stores
fixes into tests-yii2/ alongside their existing counterparts.
5.x's src/ tree no longer maps directly onto 6.x's structure (split
into src/ under the CraftCms\Commerce\ namespace and src-yii2/ under
craft\commerce\), so a normal content merge isn't meaningful here.
The four real behavioral changes since the last sync (custom field
values on order addresses, SKU {id} regeneration, the project-config
site-store race, and the editableProductTypes rename) were manually
ported to their src/ equivalents in the preceding commit. This merge
just records that origin/5.x up to 5.7.3 has been incorporated, so it
becomes the new merge-base for the next sync.
…e controllers to Ui
…ource-naming Renamed controller methods to Laravel resource naming and added new cart action URLs
Test related to craftcms/cms#19943
[6.x] Use crypto.getRandomValues for custom line item UUIDs
[6.x] Port the order index to the Inertia element index
…e field layout elements
…lugin into concerns
[6.x] Prepare Commerce toolchains and workbench for orbs
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.
https://linear.app/craftcms/issue/COM-613/laravel-port