6.x - #4124
Draft
lukeholder wants to merge 364 commits into
Draft
6.x#4124lukeholder wants to merge 364 commits into
lukeholder wants to merge 364 commits into
Conversation
New abstract Stats\Stat (port of src-yii2/Base/Stat.php) implementing the already-migrated Stats\Contracts\StatInterface directly - no Component base needed, keeps its plain positional constructor unchanged. All Yii2 Query methods (createStatQuery, createChartQuery, getChartQueryOptionsByInterval, getFirstCompletedOrderDate, getCacheKey) converted to Laravel's query builder. DB-engine branching (MySQL CONVERT_TZ vs Postgres AT TIME ZONE) converted to DB::connection()->getDriverName(). New Dashboard\Widgets\Concerns\StatWidgetTrait (port of Base/StatWidgetTrait.php). Found and fixed 2 real pre-existing bugs while live-verifying: - getCacheKey()'s dateUpdated column was ambiguous between the joined orders/elements tables - fixed by qualifying as orders.dateUpdated. - Caching used a truthiness check (if (!$data)) instead of an existence check, silently defeating caching whenever a stat's real answer is falsy (0, empty array) - fixed with Cache::has(). Nothing is wired up to these classes yet - verified via anonymous concrete subclasses exercising date-range resolution, createStatQuery(), createChartQuery(), and the cache round-trip.
Move AverageOrderTotal/NewCustomers/RepeatCustomers (stat+widget pairs) to Stats\* / Dashboard\Widgets\*. Stats' Yii2 Query usage (including NewCustomers' NOT IN (subquery) and RepeatCustomers' double-query group-count pattern) converted to Laravel's query builder. Widgets extend the new CraftCms\Cms\Dashboard\Widgets\Widget base. getSettingsHtml()'s Twig-rendered store-switcher/date-range-picker/ order-status-selectize screen replaced with settingsForm() using the new Form system's Choice controls. Added StatWidgetTrait::statSettingsFields() (store/date-range/order-status fields) as the shared building block every later widget in this stage will reuse, plus getDateRangeOptions() (drops DATE_RANGE_CUSTOM - no custom-range picker equivalent in the new Form system). getBodyHtml() keeps rendering the same legacy Twig templates via the new template() helper and registering the same legacy asset bundles via Craft::$app->getView()->registerAssetBundle() - both confirmed working unchanged from new-namespace code. init()'s setup logic moved into an overridden __construct() (the new Widget/Component base has no init() hook). Repoint 3 unit tests.
Move TopCustomers/TopProductTypes/TopPurchasables (stat+widget pairs) to Stats\* / Dashboard\Widgets\*, each with a type setting toggling between two sort/aggregate modes and rendering their body via the legacy AdminTableAsset-based table (kept as-is). Convert multi-table joins (li/p/v/pr/pt, plus users/elements_sites) to Laravel's query builder. TopProductTypes' conditional elements_sites join (column comparison AND a bound-value site-ID filter) introduces this migration's first join-closure usage, since Laravel's 3-arg join() only supports one column-to-column comparison. Found a verification-environment limitation (not a Stage 12 bug): Catalog\ProductType\ProductTypes::getViewableProductTypeIds() (Stage 7e code) calls request()->craftUser()->can(...) with no null-guard, fatal under unauthenticated console verification - real CP usage always has a logged-in user. Verified everything else live and confirmed the two blocked queries' structure via manual comparison against the original, both sharing TopCustomers' already-live-proven createStatQuery() foundation. Repoint 3 unit tests.
Move TopProducts (stat+widget), the most complex pair: a correlated subquery for per-product tax/discount/shipping adjustment totals, a dynamically-built revenue expression driven by a revenueOptions checkbox-group setting, and DB-engine-conditional IFNULL/COALESCE. createAdjustmentsSubQuery() converts to DB::table() joined into the main query via leftJoinSub() - this migration's first use of that method. getAdjustmentsSelect()/getGroupBy()/getOrderBy() keep building plain SQL strings, passed to selectRaw()/groupByRaw()/orderByRaw(). DB-engine detection converts to DB::connection()->getDriverName(). Widget's 4-checkbox revenueOptions setting (each with its own instructions + JS-driven conditional enable/disable) becomes a single Choice::make()->multiple() field with per-option instructions folded into the label text - the new Form Choice control has no per-option-instructions concept. Repoint 2 unit tests (including a leftover TopProducts import in TopProductTypesTest.php from Stage 12c, deferred until this stage).
Move TotalOrders/TotalOrdersByCountry/TotalRevenue (stat+widget pairs) to Stats\* / Dashboard\Widgets\*, the three chart-bearing widgets. Chart rendering keeps the existing server-side Chart.js pipeline (data embedded into the Twig response, same frozen-legacy StatWidgetsAsset/ChartJsAsset JS) rather than adopting cms-6 core's newer AJAX+D3 pattern. TotalOrders' plain COUNT(orders.id) scalar simplifies to createStatQuery()->count(). TotalOrdersByCountry's "other countries" negation query converts to whereNotIn(). TotalRevenue's dynamic SUM(total|totalPaid) column interpolation keeps its allow-list guard. Caught a real settings-field omission by reading the actual Twig template: TotalOrders' "Show Chart?" and TotalRevenue's "Show Order Count?" boolean settings aren't referenced anywhere in the widget PHP itself (rendered via a shared forms.lightswitchField() call in the settings twig) - would have been silently dropped from settingsForm() otherwise. Used the new Form system's Lightswitch control. TotalRevenue::defineRules() -> getRules() returning a plain Laravel validation array. Repoint 3 unit tests.
Move Orders (the "Recent Orders" widget, the only one of the 11 with no
paired Stat class - queries Order::find() directly) to
Dashboard\Widgets\Orders. Exposes storeId/orderStatuses/limit settings (no
dateRange - this widget has no date-range concept) via the new Form
system, using the Number control for limit.
Repoint all 11 use craft\commerce\widgets\* imports in
src-yii2/Plugin.php::_registerWidgets() to the new namespace.
Confirmed (not fixed): _registerWidgets() only runs on CP requests
(pre-existing getIsCpRequest() gate in Plugin.php::init()), so it never
fires under craft exec:exec - verified the Orders widget class directly
instead since the registration mechanism itself can't be observed live in
this environment.
Ran composer run fix-cs (matching Stage 10n's precedent) - 36 fixable
issues across 24 files, all pure use-line reordering plus a stylistic
`new FormContext()` parens normalization. Caught leftover import disorder
from Stage 10/11's own bulk-sed passes too, not just this stage.
Stage 12 (Dashboard Stats & Widgets) is complete. All 10 stat classes and
all 11 widget classes migrated from src-yii2/{stats,widgets}/ to
Stats\*/Dashboard\Widgets\*, every Yii2 Query/ActiveQuery usage converted
to Laravel's query builder.
…/plans Cuts over the remaining src-yii2/records/* Yii2 ActiveRecord classes to their Eloquent counterparts directly (no class_alias shim, since the two APIs are too different to bridge safely) and deletes the legacy files. Covers CatalogPricingRule/Queue, Discount, Email, Pdf, SiteStore, Coupon, TaxCategory, ShippingCategory, StoreSettings, OrderStatus, Customer, Store, and three that needed brand-new Eloquent classes: OrderNotice, Transfer, and TransferDetail. Retires three dead pivot records (CatalogPricingRuleUser, ProductTypeShippingCategory, ProductTypeTaxCategory) whose logic already lives entirely in DB::table() calls. Fixes a couple of bugs surfaced along the way (CatalogPricingQueue::getIds() call on an Eloquent model that has no such method; Payments::refund() throwing SubscriptionException instead of RefundException). Also removes all subscription and billing-plan functionality per product decision: the Subscription element, Plans, the SubscriptionGateway interface, controllers, CP screens, routes, and related events/forms/models — both the legacy Yii2 code and the already-fully-migrated CraftCms\Commerce\Subscription\* Laravel implementation. Database tables are intentionally left in place for a future data-migration path; only application code is removed. Includes an unrelated in-progress Types-registry refactor (AdjusterTypes, DiscountAdjusterTypes, GatewayTypes, PurchasableTypes) that was already sitting uncommitted in the working tree.
Cleans up src/ (new Laravel-based code) so it no longer routes through craft\* legacy classes or the Craft::$app-> service locator, per the project rule that src/ must reference current CraftCms\Cms\* equivalents directly rather than the yii2-adapter's compatibility layer. Verified each replacement against the yii2-adapter's own deprecation pointers and, where relevant, actual method-level API parity before applying it — several "obvious" swaps (ArrayHelper -> Arr, StringHelper -> Str, AuthorizationCheckEvent -> ElementAuthorizing) turned out to have divergent APIs and are intentionally left for a follow-up pass rather than guessed at. Covers: - Site, Address, Entry, PropagationMethod, ElementQuery, NameTrait, and i18n\Locale: confirmed class_alias()'d at runtime, so swapping the import is risk-free. - getTemplateMode()/setTemplateMode() -> TemplateMode::get()/::set(), with most render calls simplified further by passing TemplateMode directly as a parameter instead of mutating global state. Includes a full rewrite of Emails::sendEmail()'s 21 scattered save/restore points. - getUsers(), getSites()/getIsMultiSite(), getFields(), getFormatter()/ getFormattingLocale(), getEdition()/getUserGroups(), getDrafts(), getAddresses(), getPlugins(), getElements(), and the view namespace/JS buffer methods (setNamespace, namespaceInputs, startJsBuffer, etc.) -> their corresponding CraftCms\Cms\Support\Facades\* equivalents. - getErrorHandler()->logException()/Craft::error()/Craft::warning() -> Illuminate\Support\Facades\Log, per the project's logging docs. Removed 11 redundant logException() calls in Emails.php that duplicated an adjacent Log::error() call. getMailer() investigated but left alone: composeFromKey()'s replacement requires a User object, and both call sites send to guest email addresses with no account. Asset bundle registration (~39 call sites) also left alone per direction, since it needs a real design change to Craft 6's declarative asset system, not a swap.
Replaces Craft::$app->getDb() usages with the Illuminate DB facade: beginTransaction()/commit()/rollBack() become DB:: static calls, getIsPgsql() becomes DB::connection()->getDriverName() === 'pgsql', and Carts::purgeIncompleteCarts()'s raw createCommand()->delete() calls become DB::table()->whereIn()->delete() against materialized ID lists. Also finishes several isCpRequest()/isSiteRequest()/ session()/Log:: swaps left over from the prior batch (Customers, OrderHistories, Discounts, HasStoreManagementScreen).
registerJs($js, View::POS_END) becomes HtmlStack::js($js, Position::BodyEnd) per the legacy adapter's own deprecation notice. registerTranslations() calls are dropped entirely: the new system bulk-exports the active locale's whole translation catalog to window.Craft.translations instead of per-call registration, so these calls became no-ops.
Replaces Craft::$app->getMutex()->acquire()/release() with Laravel's Cache::lock(), per the legacy adapter's own deprecation notice (@deprecated 6.0.0. Use \Illuminate\Support\Facades\Cache::lock() instead). The old acquire($name, $waitTimeout) has no throw/bool split in Laravel: non-blocking acquisition uses $lock->get(), and blocking acquisition uses $lock->block($waitTimeout), catching LockTimeoutException where the old code checked a false return. Cache::lock() also takes a TTL (auto-expiry) that the old Yii2 mutex never had; chosen conservatively per call site (30s for DB-only critical sections, 60s where a gateway HTTP call happens inside the lock).
hashData()/validateData() are thin Crypt::encrypt()/decrypt() wrappers in the legacy adapter (confirmed by reading the implementation, not just the docblock pointer, since the @deprecated notice on hashData() incorrectly points at CraftCms\Cms\Support\Security which has no such method) so they're swapped directly to Crypt::encrypt()/decrypt(), preserving the "false on tamper" contract with a DecryptException catch at each validateData() call site. getTokens()->createToken()/getTokenRoute() become app(RouteTokens::class)->createToken()/getTokenRoute() — method signatures are unchanged, no facade exists for this service.
…tures, getSites, getConfig getLocale()->getCurrencySymbol()/getOrientation() -> I18N::getLocale(), per the legacy adapter's deprecation notice pointing at I18N::getLocale(). getStructures()->moveBefore()/moveAfter() -> app(Structures::class), same method signatures. getTimeZone() -> date_default_timezone_get(), since Yii2's own implementation is a thin wrapper around that same builtin. getSites()->getSiteById()/getEditableSiteIds() -> Sites:: facade. getEditableSiteIds() now returns a Collection instead of an array, so the in_array()/array-index call site in ProductsController is updated to ->contains()/->first(). getConfig()->getGeneral() -> Cms::config(), same mutable GeneralConfig property (generateTransformsBeforePageLoad) used the same way at every call site in Emails.php. getAssetManager()->getPublishedUrl() is left as-is: no Laravel/CMS-6 equivalent found for Yii2 asset publishing, needs follow-up design work.
Cart cookies (forgetCart/setSessionCartNumber) move from
Craft::$app->getResponse()->getCookies()->remove()/add() to Laravel's
Cookie::queue(Cookie::forget()/make()), removing the two TODOs that
were blocking on this.
OrderHistories drops the getResponse()->isSent guard: it defended
against Yii2's queue-after-response trick (continuing script execution
after the HTTP response bytes were already sent), which Craft 6's
RunQueue middleware no longer does — queue jobs now run via a separate
async XHR request instead, so the condition it guarded against can't
happen anymore.
Webhooks::processWebhook() had a latent type bug: its own return type
used yii\web\Response (via a stale import) while
GatewayInterface::processWebHook() already committed to
Illuminate\Http\Response, so the success path and the
getResponse()-based exception path returned incompatible types. Fixed
by typing the whole method to Illuminate\Http\Response and building
the exception-path response via `new Response('', $statusCode)`
(status derived from HttpException::$statusCode, matching Yii2's
setStatusCodeByException()). This also required fixing the legacy
src-yii2/services/Webhooks.php delegating wrapper, which still declared
the old yii\web\Response return type and would have thrown a TypeError
at runtime after the new class's return type changed, and
WebhooksController.php's own return type (Responsable, which
Illuminate\Http\Response never implemented).
DownloadsController's sendContentAsFile() becomes a plain
response($content, 200, [...]) with manually-set Content-Type/
Content-Disposition headers (HTTP Range support is dropped since it
wasn't load-bearing for PDF checkout downloads).
Not changed: Payments.php's getResponse()->redirect()+end() and
handleRedirect()'s echo+end() (see follow-up notes) — Craft::$app->
getResponse() and ::end() carry no @deprecated notice anywhere in the
yii2-adapter, unlike every other method fixed this session, so forcing
a redesign here isn't a confirmed migration requirement and the
existing controllers already have their own correct return-based
redirect handling for the paths that matter.
… sites)
composeFromKey() requires a real Craft User in the new
SystemMessages::mailable() convenience method, which doesn't fit these
two guest-checkout call sites (cart recovery, PDF download links sent
to an email address with no CMS user account). Verified the underlying
SystemMessageMailable class needs no User at all — that requirement is
entirely inside the convenience wrapper — so both call sites construct
SystemMessageMailable directly and send it via Mail::to($email)->send().
Also confirmed these message keys ('commerce_pdf_download',
'commerce_cart_recovery') still resolve correctly: they're registered
through the legacy Yii2 SystemMessages::EVENT_REGISTER_MESSAGES event
in src-yii2/Plugin.php, and CraftCms\Yii2Adapter\SystemMessage\
LegacySystemMessages bridges that event into the new system
automatically, so no registration changes were needed.
Laravel's Mailer::send() throws on transport failure and returns null
(rather than false) when a listener cancels the send, instead of Yii2's
bool-returning Message::send() — wrapped in try/catch and a null check
to preserve the original "show a flash error and let the user retry"
behavior on failure.
Not addressed: Emails.php's two getMailer() call sites (lines 301, 667).
Unlike the composeFromKey() cases, this is Commerce's own custom
transactional order-email pipeline: it builds a raw legacy
craft\mail\Message object by hand (from/reply-to/subject/HTML+text
body/attachments) across roughly 400 lines and sends it directly via
the mailer component. Migrating this properly means redesigning it
around a real Laravel Mailable, not a like-for-like call swap, and
deserves its own dedicated pass.
…ail pipeline)
Replaces Craft::$app->getMailer()->send($newEmail) with the exact
mechanism Craft 6's own craft\mail\Mailer::sendMessage() already uses
internally to dispatch a raw craft\mail\Message through Laravel's
configured mail transport:
app('mail.manager')->mailer()->getSymfonyTransport()->send($newEmail->getSymfonyEmail());
Found by reading the legacy Mailer's actual implementation (rather than
just its class-level @deprecated pointer, which — like hashData()
earlier this session — just says "use Laravel mailers/drivers" without
naming a concrete replacement). craft\mail\Message itself carries no
deprecation notice and still wraps a real Symfony Email under the
hood via getSymfonyEmail(), so the ~360 lines that build up $newEmail
(from/to/cc/bcc/replyTo/subject/HTML+text bodies/PDF attachment, all
via sandboxed Twig rendering) needed no changes — only how the fully
built message actually gets handed to a transport.
This also keeps MailEvent::$craftEmail (the public
beforeSendEmail/afterSendEmail extension point plugins listen to)
typed as craft\mail\Message, unchanged, since nothing about that
contract needed to move.
getSymfonyTransport()->send() throws (Symfony's
TransportExceptionInterface extends \RuntimeException extends
\Exception) instead of returning false on failure, so the explicit
`if (!...->send())` failure branch is removed — Emails::sendEmail()
already wraps this whole block in a catch (\Exception $e) with its own
(more detailed) error message and identical cleanup, so a thrown
transport failure is caught there instead.
Also simplified $newEmail's construction from
Craft::createObject(['class' => $mailer->messageClass, 'mailer' => $mailer])
to `new Message()` directly, since sending no longer goes through
craft\mail\Mailer at all and the method already hard-depends on
craft\mail\Message's own API via its calls throughout.
This closes out the getMailer() sweep — zero Craft::$app->getMailer()
call sites remain in src/.
…s equivalents craft\helpers\ArrayHelper is deprecated in favor of CraftCms\Cms\Support\Arr (which extends Illuminate\Support\Arr), but the two aren't a drop-in swap — verified each of the 14 distinct methods actually used (60 call sites across 21 files) against real signatures before touching call sites, since several diverge in shape from their Yii2 counterpart: - getColumn() -> Arr::pluck(): safe everywhere it's used here since every call site either passes keepKeys=false explicitly or consumes the result in a way that doesn't depend on key preservation (implode(), whereIn()). - merge(), toArray(), contains() -> Arr::merge()/toArray()/contains(): true 1:1 replacements, CraftCms\Cms\Support\Arr reimplements these with identical semantics to the legacy versions. - firstWhere() -> Arr::first() with a full predicate closure (Laravel's Arr has no firstWhere() — that's Collection-only). Existence-only checks (`(bool)firstWhere(...)` or `!firstWhere(...)`) became Arr::contains() instead, since that's a closer match to the actual intent. - where() -> Arr::where() with a full boolean predicate (different shape: Yii2 takes key/value/strict args, Laravel takes one callback). - index() -> Arr::keyBy() (single-key form only; no grouping used here). - map() -> Arr::mapWithKeys() with a callback returning [key => value] (Laravel's Arr::map() only transforms values in place, not keys, so it's not the right target) — or a Collection's own mapWithKeys() when the input was already a Collection instead of a plain array. - firstKey() -> array_key_first(), isIn() -> in_array(): the legacy ArrayHelper itself already points at these. - removeValue() -> array_filter() (mutation target reassigned). - multisort() -> usort() with a descending comparator (single sort key only, matching the one real usage). - prependOrAppend() -> array_unshift() (single call site, prepend only). - isAssociative() -> inlined native check, since its default mode (ALL keys must be strings) doesn't match Arr::isAssoc()'s semantics (true for ANY non-sequential key) — using the wrong one would have flipped behavior for line-item option arrays with mixed key types.
…ms equivalents
craft\helpers\StringHelper is deprecated in favor of
CraftCms\Cms\Support\Str (extends Illuminate\Support\Str). Verified each
of the 4 methods actually used (23 call sites across 16 files) against
the legacy adapter's own delegation code before swapping:
- randomString($length, $extendedChars) -> Str::random($length,
$extendedChars): the legacy method's own body is `return
Str::random($length, $extendedChars);`, and CraftCms\Cms\Support\Str
overrides random() with the same two-arg signature, so this is a
confirmed 1:1 swap, not a guess.
- toTitleCase($str) -> Str::title($str): same confirmation — the legacy
method's body delegates directly to Str::title().
- UUID() -> (string)Str::uuid(): Laravel's Str::uuid() returns a
Ramsey\Uuid\UuidInterface object, not a plain string like the old
method, so every call site needed an explicit cast to keep assigning
into string-typed properties/array values.
- split($str, $delimiter) -> inlined preg_split('/\s*' . delimiter .
'\s*/', $str, -1, PREG_SPLIT_NO_EMPTY) at each of the 3 call sites:
the legacy method itself was never delegated to the new Str class (no
Str::split() exists), it's just a raw preg_split with no wrapper, so
there's nothing to route through beyond replicating that expression
directly.
…n src/
UrlHelper -> CraftCms\Cms\Support\Url (url, cpUrl, siteUrl, actionUrl,
urlWithParams): all confirmed to exist on the new class with identical
signatures, a pure rename.
MoneyHelper -> CraftCms\Cms\Support\Money (toMoney, toDecimal, toString):
verified against the legacy adapter's own implementation, which is a
straight pass-through to the new class for every method actually used
here, a pure rename.
DateTimeHelper: toDateTime() is inherited unchanged from
CraftCms\Cms\Support\DateTimeHelper (no override), so importing the new
class directly is a pure rename. secondsToInterval() and
currentUTCDateTime() are different — they exist only on the legacy
bridge class, not the new one — so their two call sites in Carts.php
were inlined directly per the legacy source's own one-line bodies:
new DateInterval("PT{$seconds}S") and now('UTC').
Also fixed a real bug this surfaced: my first pass at the DateInterval
inlining used a bare `new DateInterval(...)` with no import, which
PHPStan caught resolving to the wrong class (CraftCms\Commerce\Order\
DateInterval) since — unlike functions — unqualified class references
in PHP do NOT fall back to the global namespace. Added `use DateInterval;`
to fix it.
Not touched: FileHelper::isWritable() (the legacy version does a real
write-attempt test via fopen(); Laravel's version is just is_writable(),
a strictly weaker check — swapping would be a behavior downgrade, not a
neutral rename) and craft\helpers\Queue::push() (wraps legacy JobInterface
jobs in a LegacyJobWrapper before dispatching; its two call sites pass
SendEmail/ResaveProductVariants jobs that haven't been migrated to
Laravel ShouldQueue jobs yet, so swapping to the bare Queue facade would
break dispatching until those job classes are migrated too).
Both classes share the "Json" short name, so this is purely an import swap for the 14 files using only encode()/decode() — both are direct pass-throughs (encode) or a verified same-contract reimplementation (decode: same InvalidArgumentException-on-bad-JSON / null-on-empty behavior, confirmed against the legacy adapter's inherited Yii2 base implementation) on the new class, so no call-site text changes needed. OrdersController.php needed real changes: its ~25 fully-qualified \craft\helpers\Json::encode()/decodeIfJson() calls became \CraftCms\Cms\Support\Json::, and its 2 htmlEncode() calls (a method that only exists on the legacy adapter, inherited from Yii2's base Json class with no delegation to the new class) were inlined as Json::encode($value, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) — the exact flag set Yii2's own htmlEncode() uses internally.
Verified all 9 methods actually used (encode, tag, a, beginTag, endTag, hiddenInput, input, hiddenLabel, namespaceInputName/namespaceInputs) before swapping. The legacy class extends \yii\helpers\Html directly with no override for encode()/input(), and the new class's __callStatic() falls back to the exact same Yii2 base class for any method it doesn't reimplement — so encode()/input() are byte-for-byte identical either way, and the 7 explicitly-reimplemented methods already have established, working call sites elsewhere in this codebase from earlier sessions. Since both classes share the "Html" short name, most files only needed their `use` statement swapped. Six files already had the new class imported as `NewHtml` (from earlier partial migration work) alongside the old bare `Html` import — those needed the bare Html:: calls renamed to NewHtml:: and the old import dropped, done via a negative-lookbehind regex to avoid double-prefixing the already-correct NewHtml:: calls (a plain find-replace would have produced NewNewHtml::). Three files used the fully-qualified \craft\helpers\Html:: form with no import; those got a proper `use` statement added and calls shortened to bare Html::.
…mfony equivalents Category A3: swaps each Yii2 framework exception (not craft\* namespace, so lower priority per CLAUDE.md, but still in scope) for its native PHP or Symfony equivalent across ~24 files. The mapping preserves the original author's own already-made distinction between "bad argument" and "bad state" at each throw site rather than re-judging every one: - yii\base\InvalidConfigException -> \RuntimeException (no perfect native equivalent — Yii2's own class just extends \Exception generically for "something about current state/config is wrong"). - yii\base\InvalidArgumentException -> native \InvalidArgumentException. Same name, but a real behavioral fix: Yii2's version actually extends \BadMethodCallException, not the native SPL class, so a catch (\InvalidArgumentException) elsewhere would never have caught it. - yii\base\Exception -> native \Exception (Yii2's own class already extends it directly with no behavior difference). - yii\base\InvalidCallException -> native \BadMethodCallException. - yii\base\ErrorException -> native \ErrorException (Yii2's own class extends it directly; constructor signature is a superset). - yii\web\HttpException / BadRequestHttpException (Webhooks.php) -> Symfony\Component\HttpKernel\Exception equivalents — same ecosystem as the Response class already used in that file, and what Laravel's own abort() throws under the hood. Required changing $exception->statusCode to $exception->getStatusCode(), since Symfony exposes it as a method, not a public property like Yii2's version. A real bug surfaced mid-sweep: unqualified native class references (new DateInterval(...), and almost RuntimeException too) do NOT fall back to the global namespace the way unqualified function calls do — PHPStan caught one resolving to CraftCms\Commerce\Order\DateInterval instead of the global class. Fixed by always using an explicit `use` import or a `\`-prefixed fully-qualified name at every throw/catch site introduced by this sweep, never a bare global class name. Left alone, with reasons (documented in updated-laravel-migration-private.md): yii\base\Event (both usages deliberately fire against a legacy class name string for third-party Event::on() listener backward compatibility), yii\mail\MailEvent (correctly paired with the legacy Mailer::EVENT_BEFORE_PREP hook, which still constructs exactly this type), yii\validators\Validator (sits inside a method explicitly marked "not yet wired up to a real validator, pending the Ruleset system" — dead code with no confirmed future type yet), yii\base\ExitException (tied to the already-flagged, not-yet-redesigned getResponse()/end() redirect flow in Payments.php), and yii\db\Expression (correctly paired with the still-legacy craft\db\Query object it's passed into — Category B2 scope, not this one).
Swaps the legacy service-locator indirection for direct container resolution across ~557 call sites in 98 files. HasServices (src/Plugin/Concerns/HasServices.php) only exists to keep src-yii2/ code working against the legacy craft\commerce\services\X wrapper classes; src/ code has no reason to route through it. The getter -> new-class mapping was derived from each legacy wrapper's own app(X::class) delegation target, not guessed. Left alone: getSettings() (framework-level Plugin/HasSettings accessor, not a service getter), the single getTransfers() site (Transfers hasn't moved to src/ yet), and ~86 event-firing chains (hasEventHandlers()/trigger()) plus two PaymentCurrencies:: convertCurrency() sites that deliberately need the legacy wrapper's inherited yii\base\Component event methods / not-yet-ported method - a first pass swapped these too and PHPStan's existing per-line ignore comments masked the breakage, so these were reverted back to Plugin::getInstance()->getX() explicitly. Also fixed a real bug this surfaced: Store/Models/Store.php's getSettings(): StoreSettings return type bare-resolves to the same-namespace Models\StoreSettings; adding a use import for the StoreSettings *service* (same short name) for an unrelated call shadowed that resolution and pointed the return type at the wrong class. Fixed by using the FQCN inline instead of importing. Verified with a full PHPStan diff (--error-format=raw) against a pre-change stash baseline, matched by (file, message) to stay immune to line-number drift from added imports - remaining differences are pre-existing type-hint gaps now attributed to the new class name instead of the legacy wrapper's, not regressions. Also ran php -l, phpstan, and fix-cs/check-cs across all touched files.
…FIG_*_KEY constants Swaps craft\commerce\services\X imports for their CraftCms\Commerce\* equivalents in the few src/ files that only reference the legacy class for a ProjectConfig CONFIG_*_KEY constant (or, for Coupons, DEFAULT_COUPON_FORMAT) - confirmed each new class already defines the identical constant. Also simplifies a few app(\Fully\Qualified\X::class) calls left over from the Category B1 pass back to bare app(X::class) now that the correct class is imported directly. craft\commerce\services\Transfers (2 sites) is deliberately left alone - Transfers hasn't been migrated to src/ yet, so no new-class target exists. src/Plugin/Concerns/HasServices.php is also left alone - its imports of the legacy service classes are intentional, not stale.
…ass_alias Swaps craft\commerce\* imports for their CraftCms\Commerce\* equivalents across 81 files, restricted to the ~52 classes confirmed runtime-identical via a literal class_alias() call in src-yii2/ (e.g. craft\commerce\elements\Order -> CraftCms\Commerce\Order\Elements\Order, craft\commerce\models\Store -> CraftCms\Commerce\Store\Models\Store) - not guessed from deprecation notices. Covers the Order element, base Purchasable/PurchasableInterface, and most Models/enums/exceptions still referenced by their legacy name. Left alone: craft\commerce\Plugin (not aliased - src-yii2's Plugin extends the new one, doesn't alias it), craft\commerce\services\* (legacy wrapper classes, some still legitimately needed - see the B1 commit), craft\commerce\web\assets\* (asset-bundle registration, already deferred), and everything else with no class_alias entry. src/Order/Adjuster/Discount.php's `use craft\commerce\adjusters\Discount as LegacyDiscount;` is deliberately excluded even though it IS aliased - LegacyDiscount::class is fired as an Event::trigger() name-string for third-party backward compatibility, and ::class resolves to the name as written, not the alias target, so renaming it would silently change which event name gets fired. Two real bugs surfaced and fixed along the way: - Several files (Inventory.php, Order.php) already had a *second*, differently-aliased import for the same target class (e.g. `Purchasable as NewPurchasable`) for a `Purchasable|NewPurchasable` union type or `instanceof` check that predates this pass. Adding a second bare import for the same class is a duplicate-type fatal (caught by php -l for type declarations) or dead code needing simplification (for instanceof/union expressions, which php -l does NOT catch - found via a full-codebase sweep for any FQCN imported under two different names). - StoreTrait.php and three model classes declared `getStore(): \craft\commerce\models\Store` inline (not via a `use` import), which PHPStan started flagging as a return-type mismatch once the *callee* (Stores::getStoreById()) was migrated to the new class name - same runtime class via class_alias, but PHPStan treats aliased classes as nominally different types. Updated all four to the new class name directly. craft\commerce\models\TransferDetail is reverted back to the legacy name in TransfersController.php specifically - it's passed into craft\commerce\elements\Transfer::addDetail(), which is entirely unmigrated legacy code still typed against the old name. Verified with a full PHPStan diff (--error-format=raw) matched by (file, message) to stay immune to line-number drift - remaining differences are pre-existing type-hint gaps in still-legacy method signatures (e.g. craft\commerce\base\Gateway), not regressions. Also ran php -l and check-cs/fix-cs across all touched files.
- src-yii2/templates/index.twig: update stale editableProductTypes() call to viewableProductTypes() (renamed pre-migration, deprecated method dropped from the new ProductTypes service) - StoresController/StoreManagementController: cast request storeId input to int before passing to strictly-typed Stores::getStoreById() - Store/Zone/Email/OrderAdjustment models: add validationData() overrides so private getter-backed attributes (name, currency, condition, to, sourceSnapshot) are actually visible to validate(), which was silently failing "required" rules and blocking saves - InventoryLocations/OrderStatuses/TaxCategories/ShippingCategories/ CatalogPricingRules/Customers/Stores: raw DB::table()->insert() calls into commerce_* pivot tables were missing dateCreated/ dateUpdated, which are NOT NULL with no DB default - Plugin.php: override cpNavIconPath() so the Commerce CP nav item gets an icon; the base implementation returns a filesystem path, but the CP's craft-icon component only resolves published icon names, so it never rendered
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