Skip to content

fix: preserve option_type_id across syncs + don't clobber attrs with empty values - #5

Merged
mageaustralia merged 3 commits into
mainfrom
fix/value-level-merge-and-options-container-guard
Jun 2, 2026
Merged

fix: preserve option_type_id across syncs + don't clobber attrs with empty values#5
mageaustralia merged 3 commits into
mainfrom
fix/value-level-merge-and-options-container-guard

Conversation

@mageaustralia

Copy link
Copy Markdown
Owner

Fixes #3, #4.

What

  1. CustomOptionsTrait — value-level title merge: incoming option values are matched to existing values by title within their option, and the destination option_type_id is preserved in the payload. Core's product-option save handler then UPDATEs the row in place instead of delete+recreate, so option_type_ids stay stable across syncs.

  2. Default options_mode flipped from 'replace' to 'merge' (3 call sites). 'replace' is still available when explicitly requested via _entity_options['options_mode'].

  3. Guard against empty overwrites of attributes with non-empty defaults: in the custom-attribute write loop, if the incoming value is empty/null AND the attribute's default_value is non-empty, the write is skipped (destination value preserved). Closes Empty source options_container overwrites destination → frontend options hidden #4 — generalises to any UI-gating attribute with a default.

Why

A single sync that carried custom_options was orphaning the entire dependent-options config on a real install (~450 products in one go) — every value's option_type_id was recreated, breaking historical order/quote item options and 3rd-party modules keyed off those IDs. The options_container clear was hiding product option dropdowns site-wide for any product whose source payload had an empty value.

Test plan

  • Trigger a sync for a product with custom options (e.g. via datasync:incremental after marking the product in datasync_change_tracker).
  • Verify catalog_product_option_type_value.option_type_id values for that product are unchanged after the sync (snapshot before/after).
  • Verify any 3rd-party module rows referencing those option_type_ids (e.g. Pektsekye-style optiondependent_value.option_type_id) remain valid after the sync.
  • For the empty-default guard: trigger a sync for a product whose source has options_container = '' and whose destination has a non-empty value — confirm the destination value is preserved.

Notes

  • Backwards-compatible across all option modes: replace deletes existing options first → new options have no $option->getId() → the existing-by-title map is empty → behaviour identical to before. merge sets option_id from title match → getId() returns the destination id → existing values fetched → title-keyed preserve kicks in. append adds only new → no existing match → same as replace path for the value step.
  • _createOptionValues() is dead code (never called from this flow — $option->setData('values', …) + $option->save() goes through core's own value handler). Left alone here; a separate cleanup PR can remove it.

…empty values

Fixes #3, #4.

- CustomOptionsTrait: match incoming values to existing values by title within
  each option and preserve the destination option_type_id in the payload, so
  core's option save handler updates rows in place instead of delete+recreate.
  Stops churning option_type_ids referenced by historical order/quote item
  options and 3rd-party dependent-option modules.

- Default options_mode flipped from 'replace' to 'merge' (3 call sites).
  'replace' is still available via _entity_options['options_mode'].

- Custom-attribute write loop now skips writes where the incoming value is
  empty AND the attribute's default_value is non-empty, preserving the
  destination value. Closes the options_container PDP-options-hidden issue.
mageaustralia pushed a commit that referenced this pull request May 27, 2026
…un read

- Adds the apply path (was previously discovery-only): reuses
  datasync/adapter_openmage + datasync/engine with on_duplicate=merge and an
  entity_ids filter, batched in configurable chunks (--batch-size, default 50).
  Same engine path as datasync:incremental, just bypassing the live tracker.

- Fixes a real bug introduced in the previous version: $dryRun was referenced
  but never assigned from $input->getOption('dry-run'), so APPLY mode always
  ran regardless of the flag. Now correctly defaults to dry-run preview unless
  --dry-run is omitted.

Safe to pair with PR #5 (value-level merge) — re-syncing a stale product no
longer churns option_type_ids, so the dependent-options config restored by the
companion CLI in maho-module-dependentoptions stays valid.
Matthew Campbell and others added 2 commits May 27, 2026 13:26
Resolve CustomOptionsTrait conflict by taking main's value-level merge
(from #8, which superseded this branch's equivalent trait change).
Retains this branch's unique changes in Product.php: options_mode default
flipped to 'merge' (4 sites) + empty-overwrite guard for attrs with a
non-empty default (closes #4).
@mageaustralia
mageaustralia merged commit ac1d5ce into main Jun 2, 2026
1 check passed
@mageaustralia
mageaustralia deleted the fix/value-level-merge-and-options-container-guard branch July 8, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant