Skip to content

6.x - #4124

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

6.x#4124
lukeholder wants to merge 451 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 26, 2026 14:56
…lidator to the new Ruleset system

Commerce 6.0 only ships Install.php for fresh installs, so the 6 loose
point-release migrations drafted in database/migrations/ are unnecessary:
4 are schema changes already present in Install.php's table definitions
(catalog pricing queue table, orders.customerDeleted, the subscriptions
userId FK cascade, order notices.noticeType), and 2 are one-time data
repairs (product type permission renaming, allVariants->variants
changedattributes cleanup) for bugs that don't exist in a fresh 6.0 schema.

CouponsValidator.php (which nothing referenced anymore) is replaced by a
closure rule on Coupon::getRules()['code'], checking case-insensitive
uniqueness against every other coupon row. It validates per-coupon inside
Coupons::saveDiscountCoupons()'s save loop rather than batch-checking the
whole Discount.coupons array like the legacy validator did, since each
coupon is validated then immediately persisted before the next one in the
loop - which naturally catches same-batch duplicates too, without needing
the legacy version's own-discountId exclusion workaround.
# Conflicts:
#	src/Http/Controllers/Settings/ProductTypesController.php
…ugin convention

HasTranslations checks dirname(getBasePath()).'/lang' before falling back to
src/translations, so plugin translations belong at the package root. Stripped
the old copyright header from each locale file to match core's app.php style.
…, changelog

Previous commit's git add -A failed silently on a stale pathspec before it
could stage the header strip and the crowdin.yml/.codecov.yml/changelog
updates, so they landed in the working tree but not the commit.
Replaces the StoreBehavior-dependent getGlobals() lookup with
Sites::getCurrentSite()->getStore(), the Site::macro('getStore', ...)
registered in Plugin::registerBehaviorMacros(). CP asset bundles and Twig
templates stay in src-yii2/ for now (separate, larger migration task).
Confirmed via cms-6's yii2-adapter (Yii2ServiceProvider::registerCraftVariableCompatibility())
that the legacy CraftVariable EVENT_INIT bridge only forwards getComponents(), not attached
behaviors, so this class never reached the live craft Twig global under Craft 6. craft.commerce/
orders/products/variants are already fully served by the NewCraftVariable macros in src/Plugin.php.
Verified live via TemplateManager::renderString() before and after removal.
The alias was left over from when this file needed to disambiguate from the
legacy craft\web\twig\variables\CraftVariable; that legacy import (and the
CraftVariableBehavior it backed) is gone now, so there's no collision. Also
fixed a stale docblock describing the pre-deletion legacy mechanism.
…umers, add opt-in serialization fields

Site/User/Address no longer support attachBehavior(), so StoreBehavior,
StoreLocationBehavior, ValidateOrganizationTaxIdBehavior, CurrencyAttributeBehavior,
CustomerBehavior, and CustomerAddressBehavior have been dead/orphaned code since
earlier migration stages stripped their EVENT_DEFINE_BEHAVIORS wiring. All six are
deleted; their functionality was already independently reimplemented via macros
and event listeners registered in src/Plugin.php.

Auditing CurrencyAttributeBehavior's *AsCurrency consumers surfaced two live
regressions (Transaction and CatalogPricing were both missing getters their own
callers already used, throwing UnknownPropertyException on the order-edit and
Settings > Product Pricing CP screens) and a third-party BC gap (ShippingMethodOption/
OrderAdjustment were public API via the legacy behavior's magic __call, independent
of Commerce's own in-repo callers) — all four now have explicit get*AsCurrency()
getters matching the pattern already established for Order/LineItem/Purchasable/Product.

Closed the serialization half of the defineFields()/defineRules() design gap (no
cms-6 core hook exists for adding keys to User::toArray()/Address::toArray()) with
four new opt-in, read-only custom field types using dbType(): null — real custom
fields flow through toArray()/GraphQL automatically once attached to a field
layout via the CP, verified live end-to-end.

Also deleted the two legacy Codeception tests asserting attachBehavior()/getBehavior()
semantics no longer possible on the new element classes, and cleaned up dangling
IDE-hint-only references to the deleted classes.
Also fixes src/Catalog/Products.php, which was referencing the legacy
craft\commerce\Plugin class instead of the new CraftCms\Commerce\Plugin.
CKEditor's own field class is still Yii2-based (not yet ported to Craft 6),
so the listener still registers via the legacy yii\base\Event::on()
mechanism -- there's no Laravel event to hook yet. Redactor isn't supported
under Craft 6, so its registration is deleted outright rather than ported.
src/ previously had a three-way Records/Models/Data split per domain
folder, with Models/ being an inconsistent mix of Eloquent persistence
models and plain Yii2-style data/config objects. This collapses it to
a consistent two-namespace convention: Models/ is Eloquent only
(extends BaseModel), Data/ is everything else (extends Component),
matching the existing Catalog/ProductType/Data and Order/LineItem/Data
precedent. Records/ is gone.

Also moves Tax\Models\EuVatIdValidator to Tax\VatValidator\Eu, pulling
it out of this convention since it's a stateless validator, not a data
object.

Pure rename, no behavior changes, no BC shims (CraftCms\Commerce\* is
still unreleased/alpha).
Ports craft\commerce\Plugin::_registerProjectConfigEventListeners() to
CraftCms\Commerce\Plugin::registerProjectConfigEventListeners(),
registering against app(CraftCms\Cms\ProjectConfig\ProjectConfig::class)
instead of the legacy Craft::$app->getProjectConfig().

The new ProjectConfig::onAdd()/onUpdate()/onRemove() invoke handlers
with ItemAdded/ItemUpdated/ItemRemoved (subclasses of the new
CraftCms\Cms\ProjectConfig\Events\ConfigEvent), not the legacy
craft\events\ConfigEvent, so every handler method this wires up had
its type-hint updated to match. The legacy src-yii2/services/* shims
keep accepting the legacy ConfigEvent on their own signatures for
third-party BC, adapting it into the new type before delegating.

Also modernized ProductTypes::pruneDeletedSite() to listen for the
Laravel SiteDeleted event instead of the legacy
craft\services\Sites::EVENT_AFTER_DELETE_SITE, and the project-config
rebuild listener now uses the Laravel ProjectConfigRebuilt event.
The Emails::EVENT_AFTER_DELETE_EMAIL listener stays on the legacy
yii\base\Event::on() mechanism for now, since that event's firing
hasn't been bridged to Laravel yet (see the TODO in
Emails::handleDeletedEmail()).
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
nfourtythree and others added 30 commits October 9, 2026 16:15
[6.x] Render product sources in the secondary nav
# Conflicts:
#	src/Plugin.php
…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
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