Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /changelog/Changelog.4.111.md for this page in Markdown format</div>
Version 4
version 4.111
[4.111.3] fix(build): resolve eagerly-loaded jsCapabilities chunks to their content-hashed files (Advisable-com/ecommercen#379 follow-up)
- The regression. #379 added
output.chunkFilename: '[name].[contenthash].js', which madefront-mix-manifest.jsonkeyed by the hashed chunk name. Butapplication/config/main.php'sjsCapabilitiesblock — rendered eagerly as<script>viaassetUrl($src, '000')inrenderJsCapabilities(), e.g.pushJsCapability('checkout-page')in the checkout view — looks chunks up by their bare path (/ui/main/js/checkout-page.js).assetUrl()'s exact-key manifest lookup then missed and fell back to…/checkout-page.js?v=000: a frozen cache-buster on a bare file the build no longer regenerates. The eager checkout-page (and 6 other capability) scripts thus regressed from properly busted (?id=<contenthash>) to permanently stale. - The fix. A
mix.then()post-build step inwebpack.mix.front.jsaugmentsfront-mix-manifest.jsonwith a stable bare-name alias for every content-hashed chunk (/ui/main/js/checkout-page.js→/ui/main/js/checkout-page.<hash>.js).assetUrl()now resolves the eager (jsCapabilities) path to the same hashed file the webpack runtime loads lazily — both paths are content-addressed and consistent. Mirrors the bare→hashed mapping the planned Vite migration provides natively. - No DB / REST / language changes. Admin is unaffected (no jsCapabilities).
- Verify on merge: after a production build,
front-mix-manifest.jsoncontains"/ui/main/js/checkout-page.js": "…/checkout-page.<hash>.js…"; loading checkout issues a single request to the hashedcheckout-page.<hash>.js(no?v=000bare request, no 404).
- The regression. #379 added
[4.111.3] fix(build): content-hash webpack code-split chunks to stop stale-asset checkout outages (Advisable-com/ecommercen#379)
- The bug.
laravel-mix .version()versions only manifest entries (the?id=query strings resolved byassetUrl()), not the runtime code-split chunks loaded by the webpack runtime (public/ui/main/js/manifest.js). Those chunks (checkout-page, etc.) kept bare, unhashed filenames across releases, so when a chunk's content changed its URL did not — CDNs/browsers served the stale chunk, breaking checkout across clients (both pipeline- and dev-built). Upstream: laravel-mix #3186. - The fix.
webpack.mix.front.js/webpack.mix.admin.jsaddmix.webpackConfig({ output.chunkFilename: '<dir>/[name].[contenthash].js', optimization.{moduleIds,chunkIds}: 'deterministic' })(frontui/main/js/, adminui/admin/dist/), so dynamically-imported chunks are content-addressed — a content change yields a new URL. Deterministic ids keep chunk identity stable so the hash tracks real content, not id churn. Entry bundles and.combine()/.version()/mix-manifestbehavior are unchanged. (The eagerjsCapabilitiesload path is handled by the follow-up entry above.) - Mid-deploy hardening.
assets/main/vue/vueapp.jswraps the lazy async-component import factories in alazyChunk()helper that, on aChunkLoadError, performs a singlesessionStorage-guardedlocation.reload()to fetch the fresh manifest + chunks (one reload per page-life and per session; no loop if the chunk is genuinely gone; re-arms on a successful load). - No DB migration, REST API change, or language-key changes. Stopgap for the broader Vite migration (content-hashes chunks by default), tracked separately.
- The bug.
[4.111.3] fix(blog): set
isBlogon author pages so they get the blog storefront treatment (Advisable-com/ecommercen#385)- The bug. In
ecommercen/blog/controllers/Adv_blog.php,blogAuthor()was the only blog action that omitted'isBlog' => truefrom its render array — its siblingsindex(),blogArticle(),blogCategory(),blogTag(), andsearchPage()all set it. The omission caused blog author pages to miss theisBlog-gated storefront treatment: blog LD+JSON structured data (application/views/production/ld_json.php) and blog-search/priority-nav JS under solrSearch v1 (application/views/main/components/footer/footer_js.php). - The fix. Added
'isBlog' => truetoblogAuthor()'s render array, consistent with the sibling actions. One-line change; no DB migration, REST API change, or language-key changes.
- The bug. In
[4.111.3] fix(category): pass
$recordtogetMuiEditPostso non-ADVISABLE edits preserve the category slug (Advisable-com/ecommercen#386)- The bug. In
ecommercen/category/controllers/Adv_category_admin.php,getMuiEditPost()referenced$recordwhich was only a local variable inedit()and was never passed to the hook. For ADMIN/CMS admins the slug-preservation guard$hasSlugtherefore always evaluated false, causing the slug to be silently regenerated viacreate_slug()on every save — overwriting manually-customized slugs and changing the category URL whenever the name was edited. ADVISABLE-role admins were unaffected (they derive$hasSlugfrom the posted slug field). - The fix.
getMuiEditPost()now accepts an optional$record = nullparameter (backward-compatible).edit()passes the already-fetched$record, whose MUI rows are attached as$record->{lang}bygetAdminRecord(), so$record->{$langAbbr}->slugcorrectly detects an existing slug and preserves it. No other callers or client overrides of this method exist. - No DB migration, REST API change, or language-key changes.
- The bug. In
[4.111.3] feat(eshop): make category Agora sponsored slot positions config-driven (Advisable-com/ecommercen#380)
- What & why. The category listing's Agora sponsored-product slot positions were hardcoded inline in
Adv_product_categories::projectAgoraAll()($isCat0 ? [1, 2, 3, 4] : [1, 2, 3, 9, 13, 15, 21]), so a shop wanting a different slot layout had to full-copy the ~106-line method into an override. That is exactly the merge-drift that silently orphaned a client override when 4.x consolidated the old per-featureprojectAgora*methods into the asyncprojectAgoraAll(). Realizes those methods' own// @TODO move to config. - The change.
projectAgoraAll()now reads the slot list from two config keys with inline fallbacks to the previous defaults —agoraSponsoredPositions(non-cat0 pages) andagoraSponsoredPositionsCat0(cat0 pages) — registered inapplication/config/app.phpwith the historical arrays. A shop overrides just the config value (in its own config) — no code override. Default output is byte-identical for every existing shop. - Housekeeping. Removed the now-orphaned legacy methods superseded by
projectAgoraAll()that still carried the duplicate hardcoded arrays / per-feature flows:projectAgoraItems(),projectAgoraItemsCat0(),projectAgoraBanners(),projectAgoraDsplItems(), andprojectAgoraDsplItemsV2()(its dsplFirst+dsplSecond merge is now done inline byprojectAgoraAll()). Verified zero callers across the repo (the same method names inAdv_search/Adv_vendors/Adv_orderare those controllers' own copies and are untouched). Also dropped a stale leftover docblock aboveprojectAgoraAll(). - No DB / route / DI / API-contract / frontend / language changes.
- Verify on merge: category pages render sponsored products in the same slots as before with no config override; setting
$config['agoraSponsoredPositions'] = [1, 2, 3, 9, 13, 15, 21, 22, 23, 24]lands the 8th–10th sponsored products in slots 22/23/24 instead of being appended at the end.
- What & why. The category listing's Agora sponsored-product slot positions were hardcoded inline in
[4.111.3] refactor(apifon): resolve Apifon through the DI container
- What changed.
Advisable\Apifon\Apifonis now a Symfony DI service registered insrc/Apifon/container.php(wired intoapplication/config/container/modules.php) and resolved viadi()->get(Apifon::class)at all call sites, replacing the previous manualnew Apifon(...)+ CI-registry construction. The named'apifon'logger (NamedLoggerInterface) is now constructor-injected into the service; the old in-bodydi()logger fallback is gone. - Call sites updated.
ecommercen/helpers/sms_helper.php(2 call sites) andecommercen/eshop/controllers/Adv_order.php(1 call site) now obtain the instance throughdi()->get(Apifon::class). \Registrystays out of the container — by design. CI's library loader (system/core/Loader.php_ci_init_library) resolves libraries through the container by bare class name: ifdi()->has($className)it callsdi()->get($className)instead ofnew $className(). Because theregistrylibrary's class name is exactlyRegistry, adding a service under that id hijacks CI's own library load and recurses at boot (500 on every page). ARegistryFactoryapproach was attempted and reverted.\Registryis therefore resolved lazily at request time via a privateregistry()helper on the service (same pattern asDomains\Features\FeatureRegistry), not injected as a constructor dependency. A NOTE documenting this constraint is now inapplication/config/container/container.php.- No externally visible changes — no REST API version bump, no DB migration, no language-key changes.
- Follow-up. The same lazy-DI pattern should be applied to the remaining manually-
new'd integrations: Moosend (#381), Manago (#382), BunnyStream (#383), ShopflixOrders (#384).
- What changed.
[4.111.1] fix(rest): localize REST payment-gateway order descriptors to match the legacy storefront
- The divergence. Placing the same order via the storefront vs. the REST API produced different descriptions on the acquirer statement. The legacy
Adv_checkoutcontroller renders localized descriptors viat()keys; the REST payment adapters hardcoded fixed-English strings ("Order {serial} - {site}","Order #{serial}", etc.). Surfaced by a client (Seajets) reconciling Nexi statements. Cosmetic only — order matching keys onorder_serial/tran_ticket/securityToken, not the descriptor — but inconsistent across checkout paths. - The fix. Each adapter whose legacy counterpart uses a localized
t()descriptor now calls the same key with the same argument order, so the descriptor is byte-for-byte identical and localized for both paths:XPayAdapter→checkout.xpay.order_description(Nexidescription).CardLinkAdapteralpha →checkout.alpha.order_description(via a new optionalorderDescriptionKey); eurobank keeps the legacy"{site} Order {serial}"literal it already matched.EthnikiAdapter/EthnikiEEAdapter→checkout.ethniki.order_description(Simplifydata-name/ NBGorder.description).VivaWalletAdapter→t('hct_eshop_name') . ' ' . serial(customerTrns).PayPalExpressAdapter→t('hct_paypal_express_description')(description; serial still carried inreference_id).
- All keys already exist in the 8 locale files — no language changes. Adapter
orderDescriptionPrefixconstructor params renamed tositeNamefor consistency withXPayAdapter; the factory passesconfig_item('site_name')to each. - Deliberately unchanged (no localized legacy descriptor to match):
PayPalAdapter(advanced — order built client-side via the JS SDK),StripeAdapterandKlarnaAdapter(real per-product line items; the unusedcheckout.klarna.order_descriptionkey is left as-is), andEthnikiNbgPayAdapter(sends no gateway descriptor). - Tests. Updated the adapter tests to assert the localized descriptors; added
VivaWalletAdapterTestand a CardLink alpha-path case. Added a namespace-scopedt()shim (tests/Support/t_helper_shim.php, mirroring the existingconfig_itemlocale shim) so the pure-Unit suite resolves the adapters't()calls deterministically and the Integration suites delegate to the real CI helper. - Verify on merge:
vendor/bin/phpunit tests/Unit/Checkout/Payment/Adapters/— all adapter descriptor tests green (incl. the newVivaWalletAdapterTestand the CardLink alpha-path case).- Place a test order through XPay / Alpha / Ethniki / EthnikiEE / Viva / PayPal-Express via both the REST API and the storefront, and confirm the descriptor matches across paths — e.g. XPay →
xpay_logging.request_dataorder.descriptionreads"Transaction {serial} from {site}"; CardLink alpha → gatewayorderDesc; Viva →customerTrnsreads"{eshop_name} {serial}". In a Greek-locale deployment the descriptor should render in Greek, not English. - Confirm the deliberately unchanged adapters still behave: Eurobank sends the
"{site} Order {serial}"literal; PayPal-advanced / Stripe / Klarna descriptors are untouched. - No DB migration and no new lang keys — every
checkout.*.order_descriptionandhct_*key already ships in all 8ecommercen/language/*/files.
- The divergence. Placing the same order via the storefront vs. the REST API produced different descriptions on the acquirer statement. The legacy
[4.111.0] feat(rest):
GET /rest/storefront-config— governed structured-settings resource (Advisable-com/ecommercen#374)- What it is. A curated, typed, sectioned REST resource exposing backend-owned structured storefront settings to headless clients — the non-boolean sibling of
/rest/features. Where/rest/featuresis a flat boolean capability map (withFeatureGuardMiddlewareenforcement), this carries parameter values (numbers, lists, strings) grouped into named sections, with no server-side gating. Guest auth, read-only, global (not per-user/locale). REST API bumped to v1.12. - First section
listing.{ perPageDefault, perPageOptions[], perPageMax }derived live from the storefront config (products_list_limit/pageLimitsArray()/products_list_max) so a headless client renders its per-page selector identically to the rendered storefront and tracks whatever the deployment configures — no rebuild. Default deployment returns{ perPageDefault: 36, perPageOptions: [9, 18, 36, 72], perPageMax: 72 }. - Governed like
features, not a registry dump. Sections/keys are curated and typed in code (Advisable\Domains\StorefrontConfig\StorefrontConfigProvider) and mirrored by the OpenAPI schema; each new section/key is a deliberate, reviewed contract addition (same discipline as adding a feature key). It is not an arbitrary config/registry passthrough — exposed values get a review gate and the frontend stays decoupled from internal config names. Platform-owned shape, destined forvelora-contracts-base. - Why a sectioned resource (not a per-setting endpoint). Many backend-owned settings will need exposing over time (per-page, default locale/currency, free-shipping threshold, loyalty point factors, …). A new typed section slots in with no new endpoint each time. Kept separate from
/rest/features(different semantics — values vs gates, no middleware enforcement). - Consumer. velora#269 — wired via a general
useStorefrontConfig()with anapp.config.tsfallback (catalog.perPage= default 36 / options [9,18,36,72] / max 72); picks up this resource automatically once available (backend-over-fallback, same asfeatures). No velora redeploy needed when it lands.
- What it is. A curated, typed, sectioned REST resource exposing backend-owned structured storefront settings to headless clients — the non-boolean sibling of
[4.111.0] fix(test): stop
ThemeHelperProductImagesJsonTestconfig stub self-recursing on non-base_urlkeys (Advisable-com/ecommercen#375)- The bug. The test's anonymous config stub delegated every key except
base_urlback throughget_instance()->config->item($key). But the samesetUp()registers that stub asget_instance()(GetInstanceRegistry::set($this->ciStub)), so the delegation re-entered the stub and recursed until Xdebug aborted at 512 frames — erroring all 21 tests. Upstream stayed green only because itsassetUrl()readsbase_urlalone; a client whoseMY_url_helper::assetUrl()override reads another key (e.g.config_item('app_version')as an asset cache-buster, as in the wecare fork) tripped it, masking the #325 JSON-shape and #372 resize-key coverage on those forks. - The fix. Capture the real CI config object before the
get_instance()swap and delegate non-base_urlkeys to it, preserving the original intent.base_urlstays stubbed for predictable assertions; every other key resolves from the real bootstrapped config regardless of which keys a client'sassetUrl()reads. Added a regression test that reads a non-base_urlkey through the swapped instance to pin the no-recursion contract (verified red against the old delegation, green against the fix). Test-harness only — no runtime code changed.
- The bug. The test's anonymous config stub delegated every key except