Skip to content

6.x - #4124

Draft
lukeholder wants to merge 463 commits into
5.xfrom
6.x
Draft

6.x#4124
lukeholder wants to merge 463 commits into
5.xfrom
6.x

Conversation

@lukeholder

@lukeholder lukeholder commented Sep 23, 2025 •

Copy link
Copy Markdown
Member

@lukeholder
lukeholder requested a review from a team as a code owner September 23, 2025 07:42
@lukeholder
lukeholder marked this pull request as draft September 23, 2025 07:42
@lukeholder
lukeholder removed the request for review from a team August 19, 2026 10:07
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.
lukeholder and others added 25 commits August 28, 2026 07:55
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.
lukeholder and others added 30 commits October 12, 2026 15:04
…ource-naming

Renamed controller methods to Laravel resource naming and added new cart action URLs
[6.x] Use crypto.getRandomValues for custom line item UUIDs
[6.x] Port the order index to the Inertia element index
[6.x] Prepare Commerce toolchains and workbench for orbs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants