FormTrait uses trait:short, trait:short, trait:short, trait:short
Edit form of a product set, the main sales entity of the catalog.
It covers the "Set" tab (Templates/Default/Default.latte): basics, texts, gallery, set
composition, accessories, parameters and tags, fees and links. The price list, comments, ratings
and stock are sibling tabs of this component (PriceSnapshotTrait, StockOperationTrait)
and are not part of this form. The VAT rate is determined by TaxManager from the supply kind.
The stock card is always created automatically, without a picker or checkbox.
Saving combines two paths:
- The object graph (
api/eshop/product-set): scalars, texts, set composition, accessories and four many-to-many collections. Owned collections haveorphanRemoval, so they are sent complete;pricesandvatsare in the ApidefaultSkipFieldsand the graph save never touches them. $meta['images'], the gallery (base64 to Asset), see ImageFieldsTrait.
The Api afterSave creates and maintains the comment and rating thread; on the Admin entity
the FK is #[Transient], so no save overwrites it.
Table of Contents
Properties
- $fieldBuilder : ParameterFieldBuilder|null
- $parameterFormItems : array<string|int, ParameterFormItem>
- $parameterFormPrepared : bool
Methods
- createComponentEditForm() : Form
- parameterRowPicker() : PickerConfig
- Returns the picker config for a multiplier row: suggest over root parameters (excluding the already picked) plus a modal on `:Admin:Eshop:Parameter`.
- resolveSupplyKindDefaultId() : int|null
- addParameterRowFields() : void
- Adds the `parameterRows` multiplier: hidden `parameterId` (filled by the picker) and text `parameterName` (label that survives AJAX).
- addProductSetAccessoryFields() : void
- Adds the accessory rows: picker of another set; the order is the row order (SortableJS).
- addProductSetBaseFields() : void
- Adds the basics: codes, unit, type, owning product, state, publication and goods parameters.
- addProductSetImageFields() : void
- Adds the hidden dropzone temp folder token (on an AJAX re-render after a validation error {@see resolveUploaderToken()} recovers it from the POST) and the form `enctype`; the classic fallback below sends files as multipart even though the dropzone itself uploads through fetch/FormData outside the form.
- addProductSetItemFields() : void
- Adds the set composition rows: picker of another set plus quantity (`set_item_relations.count`).
- addProductSetM2mFields() : void
- Adds the plain many-to-many collections, each as a multiplier of pickers.
- addProductSetTranslationFields() : void
- Adds the multilingual fields, one set per language (`<field>_<langId>`).
- addVariantNotice() : void
- Adds a variant save message that is visible in the modal content.
- applyProductSetParameterRels() : void
- Replaces `parameterRels` fully; `parameter` is a thin id reference that the Api hydrator resolves as a managed entity.
- applyUnit() : void
- Writes the catalog choice and, at the same time, the print abbreviation.
- buildEshopCurrencyItems() : array<int, string>
- buildEshopParameterContext() : ParameterFormContext
- buildParameterRowDefaults() : array<int, array{parameterId: int, parameterName: string}>
- Returns the prefilled rows: one row per distinct parameter with a value in the edited set (a multi-value parameter is one row).
- buildProductSetAccessories() : ArrayCollection<int, ProductSetAccessory>
- Builds the owned accessory collection (PK set plus accessory, `order`).
- buildProductSetEntity() : ProductSet
- Builds the set with all its collections. On edit it works with the loaded entity (it carries associations the form does not edit, which the object-graph save would otherwise null).
- buildProductSetItems() : ArrayCollection<int, SetItemRel>
- Builds the owned `SetItemRel` collection from the composition rows (PK set plus item, `count`).
- buildProductSetM2m() : ArrayCollection<int, T>
- Builds thin id references for a plain many-to-many collection; the Api `hydrateForPersist` resolves them as managed entities and rewrites the join table.
- buildProductSetParameterRows() : array<int, array{id: int, value: string, value2?: string}>
- Converts the submitted values to flat id/value/value2 rows via {@see ParameterFieldBuilder::extractValues()}.
- buildProductSetRelationRowDefaults() : array<string, array<int, array<string, mixed>>>
- Returns the multiplier prefill rows of all relation collections from the loaded set.
- buildVariantModeAfterSave() : callable
- Returns the post-save hook for mode `product-variant`, which does no page redirect (the modal is a dedicated shell above the presenter page): - `save` (AJAX): closes the modal (`payload->modalClose`); - `update` (AJAX): keeps the modal open and redraws the whole component (fresh `$editedSet`, see {@see saveProductVariantForm()}); always also redraws the variant list and flash on the presenter; - non-AJAX fallback (JS disabled): a full PRG to the product Variants tab ({@see prgAnchor()}).
- cleanupImageUploader() : void
- Deletes the dropzone temp folder after a successful save.
- collectProductSetImageMeta() : array<int, array{assetId: int, image?: array{name: string, data?: string, path?: string}}>
- Builds `$meta['images']`, the sorted gallery rows (unchanged Api contract, see the trait docblock).
- configureImageUploader() : void
- Configures the dropzone uploader; called from the component factory on every request, also on the upload/remove AJAX signals where the rest of the form is not built. Existing images come only from the loaded set (edit); a new set gets an empty dropzone.
- entityReference() : T|null
- Returns a light reference to an entity (id only) that the Api hydrator resolves as a managed entity.
- extractSubmittedParameterIds() : array<int, int>
- Returns the deduplicated positive IDs from the submitted `parameterRows[*][parameterId]`; non-row multiplier keys are skipped.
- flashRejectedImages() : void
- Flashes gallery images that the Api rejected (type, dimensions, unreadable); the save went through without them. Uses the presenter channel (`flashMessagesSnippet` is redrawn after a save in both modes).
- getDefaultUnitId() : int|null
- Returns the default unit for a new product: the first catalog unit by order.
- getFieldBuilder() : ParameterFieldBuilder
- getProductSetParameterValues() : array<int, mixed>
- Returns the prefill values (`parameterRels`) shaped by parameter type.
- getUnitOptions() : array<int, NotTranslate>
- Returns the unit options for the select: `unit_id => "pcs (piece)"`. Only active items: a deactivated unit is not offered for a new choice, but products that already have it keep it.
- guardRelationRowLimit() : void
- Guards the row limit of one of the five relation collections, measured on the raw HTTP data in `onAnchor` (see {@see MultiplierLimits::guard()} for why `onAnchor`).
- isProcessableImageData() : bool
- Returns whether GD can process the base64 image (non-fatal validation).
- isProcessableImageFile() : bool
- Same check for a file from the dropzone temp folder. Only the `path` reference goes to the Api; the content is read one file at a time and released immediately.
- loadEshopParameterGroups() : array<string|int, ParameterGroupInterface>
- loadEshopParameters() : array<string|int, ParameterFormItem>
- Loads the active root parameters for the given IDs, without rule graphs.
- loadProductSetSelectData() : void
- Loads the form axes (currencies, groups, regions, VAT, states) once per request, before the fields are built.
- parameterDisplayName() : string
- prepareExistingAssets() : array<int, array{imageId: int, thumbPath: string, isMain: bool, order: int}>
- Returns the existing set images for the dropzone, sorted by `order` (`order === 1` is the main photo, see {@see \App\Core\Base\Shared\Eshop\Models\Entities\ProductSetImageRel::isMain()}).
- prepareParameterForm() : void
- Loads the active parameters (by multiplier rows or the edited set), their groups and the context, and initializes the builder.
- prgAnchor() : string
- Returns the anchor for a non-AJAX PRG redirect (snapshot delete, "create card" etc.).
- redrawVariantModal() : void
- Redraws the host snippet `setEditHost` on an AJAX submit error (validation or save failure).
- redrawWizardStep() : void
- Redraws one wizard step: "add/remove tier" in step 2 (price matrix, `$onChange` of {@see PriceSnapshotTrait::buildPriceTierMatrix()}) invalidates only the snippet `wizardPriceStep`, not the whole component, so fields typed in other steps stay untouched in the DOM.
- reloadEditedSet() : void
- Reloads `$this->editedSet` through the canonical `EDIT_INCLUDES`.
- resolveActiveParameterIds() : array<int, int>
- Returns the IDs of parameters whose typed field is built.
- resolveDuplicateParameters() : array<int, Parameter>
- Returns parameters for prefilling a new variant opened in a duplicate context (`$duplicateFromSetId`).
- resolveEditedSetParameters() : array<int, Parameter>
- Returns the distinct parameters of a set (by `parameterRels`) sorted by `lft`, so the row order is stable.
- resolveParameterLabel() : string|null
- resolveProductCategoryParameters() : array<int, Parameter>
- Returns parameters for prefilling a new variant: the Api bridge `parameters-for-product` aggregates over all product categories (distinct, ordered by minimum `order`).
- resolveUploaderToken() : string|null
- Returns the dropzone temp folder token: null on a fresh GET (the uploader generates one in `configureImageUploader()`), on a POST (submit or validation error) it is read from the hidden field so the same temp folder is reused.
- saveProductSetForm() : bool
- saveProductVariantForm() : bool
- Saves mode `product-variant`: two entities through their own CRUD stack, in this order.
- setProductSetFormDefaults() : void
- suggestParameters() : array<int, array{id: int, label: string}>
- Suggests root parameters by name, skipping the already picked ones on POST: the typed `parameter_X` field exists once per parameter.
- unitName() : string
- Returns the unit name in the current language, or the first non-empty translation when missing.
- variantBelongsToContextProduct() : bool
- Returns whether the variant `$variantId` (deleted or not) belongs to the context product `$productId`.
- wireProductSetMultiplierButtons() : void
- Wires the add/remove buttons of a multiplier: UI-only actions (naja redraws just its snippet), hence `setValidationScope([])`; otherwise removing a row would raise required-field errors elsewhere.
Properties
$fieldBuilder
private
ParameterFieldBuilder|null
$fieldBuilder
= null
$parameterFormItems
private
array<string|int, ParameterFormItem>
$parameterFormItems
= []
$parameterFormPrepared
private
bool
$parameterFormPrepared
= false
Methods
createComponentEditForm()
protected
createComponentEditForm() : Form
Return values
FormparameterRowPicker()
Returns the picker config for a multiplier row: suggest over root parameters (excluding the already picked) plus a modal on `:Admin:Eshop:Parameter`.
protected
parameterRowPicker() : PickerConfig
Picking a parameter clicks parametersRefresh to render its typed field.
Return values
PickerConfigresolveSupplyKindDefaultId()
protected
resolveSupplyKindDefaultId(SupplyKindManager $manager, array<int, mixed> $options[, string|null $preferredKey = null ]) : int|null
Parameters
- $manager : SupplyKindManager
- $options : array<int, mixed>
-
select options (
SupplyKindManager::findSelectOptions()) - $preferredKey : string|null = null
Return values
int|nulladdParameterRowFields()
Adds the `parameterRows` multiplier: hidden `parameterId` (filled by the picker) and text `parameterName` (label that survives AJAX).
private
addParameterRowFields(Form $form) : void
The typed value field is not part of the row; the template renders the builder's parameter_X inline.
Parameters
- $form : Form
addProductSetAccessoryFields()
Adds the accessory rows: picker of another set; the order is the row order (SortableJS).
private
addProductSetAccessoryFields(Form $form) : void
Parameters
- $form : Form
addProductSetBaseFields()
Adds the basics: codes, unit, type, owning product, state, publication and goods parameters.
private
addProductSetBaseFields(Form $form) : void
Parameters
- $form : Form
addProductSetImageFields()
Adds the hidden dropzone temp folder token (on an AJAX re-render after a validation error {@see resolveUploaderToken()} recovers it from the POST) and the form `enctype`; the classic fallback below sends files as multipart even though the dropzone itself uploads through fetch/FormData outside the form.
private
addProductSetImageFields(Form $form) : void
Parameters
- $form : Form
addProductSetItemFields()
Adds the set composition rows: picker of another set plus quantity (`set_item_relations.count`).
private
addProductSetItemFields(Form $form) : void
An item may be a ready bundle (nested set); recursion and cycles are checked by the Api
(ProductSetManager::validateComposition() and SetCompositionService).
Parameters
- $form : Form
addProductSetM2mFields()
Adds the plain many-to-many collections, each as a multiplier of pickers.
private
addProductSetM2mFields(Form $form) : void
The legacy parameters (specification) collection is not edited here; the tab uses the
dynamic fields of ParameterFieldsTrait.
Parameters
- $form : Form
addProductSetTranslationFields()
Adds the multilingual fields, one set per language (`<field>_<langId>`).
private
addProductSetTranslationFields(Form $form) : void
Parameters
- $form : Form
addVariantNotice()
Adds a variant save message that is visible in the modal content.
private
addVariantNotice(string $key[, string $type = 'danger' ]) : void
Result-level errors (duplicate code, VARIANT_TAKEN, cycle, generic) are not bound to a field
(they are not Nette field errors, so {inputError} does not show them) and the presenter
flashMessage() is structurally invisible in the component template
(see ProductSetEdit::$formNotices).
They are therefore written to a buffer rendered in setEditHost (Default.latte). The presenter
flashMessage() is kept as the non-AJAX fallback and global channel (banner after a reload).
Parameters
- $key : string
- $type : string = 'danger'
applyProductSetParameterRels()
Replaces `parameterRels` fully; `parameter` is a thin id reference that the Api hydrator resolves as a managed entity.
private
applyProductSetParameterRels(ProductSet $set, array<int, array{id: int, value: string, value2?: string}> $rows) : void
Always called, even with empty $rows (remove all): the submitted rows are the whole
truth, unlike Bazaar where parameters outside the chosen category are absent from the form.
The identity of rows is carried over: a new rel takes the id of a stored rel of the same
parameter, preferring one with the same value, so a changed value is an update, not delete
plus insert. The hydrator pairs owned children by id
(OrmEntityHydrator), so an unchanged save leaves
product_set_parameter_rel untouched; unused old rels are removed by orphan removal.
Parameters
- $set : ProductSet
- $rows : array<int, array{id: int, value: string, value2?: string}>
applyUnit()
Writes the catalog choice and, at the same time, the print abbreviation.
private
applyUnit(ProductSet $set, mixed $unitId) : void
unit (text) is what goes into the order and invoice. It is kept denormalized on the product
so that deleting a unit from the catalog does not break history or invoice regeneration.
Without a valid choice the text is not overwritten: an existing product keeps it, a new one
falls back to ks.
Parameters
- $set : ProductSet
- $unitId : mixed
buildEshopCurrencyItems()
private
buildEshopCurrencyItems() : array<int, string>
Return values
array<int, string>buildEshopParameterContext()
private
buildEshopParameterContext(array<string|int, ParameterFormItem> $items) : ParameterFormContext
Parameters
- $items : array<string|int, ParameterFormItem>
Return values
ParameterFormContextbuildParameterRowDefaults()
Returns the prefilled rows: one row per distinct parameter with a value in the edited set (a multi-value parameter is one row).
private
buildParameterRowDefaults() : array<int, array{parameterId: int, parameterName: string}>
Without an edited set, the rows come from resolveDuplicateParameters().
Return values
array<int, array{parameterId: int, parameterName: string}>buildProductSetAccessories()
Builds the owned accessory collection (PK set plus accessory, `order`).
private
buildProductSetAccessories(ArrayHash<string|int, mixed> $values, ProductSet $set) : ArrayCollection<int, ProductSetAccessory>
The order is the row position in the submit (SortableJS moves the DOM, the browser sends rows in the new order).
Parameters
- $values : ArrayHash<string|int, mixed>
- $set : ProductSet
Return values
ArrayCollection<int, ProductSetAccessory>buildProductSetEntity()
Builds the set with all its collections. On edit it works with the loaded entity (it carries associations the form does not edit, which the object-graph save would otherwise null).
private
buildProductSetEntity(ArrayHash<string|int, mixed> $values[, int|null $variantModeVariantId = null ]) : ProductSet
Parameters
- $values : ArrayHash<string|int, mixed>
- $variantModeVariantId : int|null = null
-
only mode
product-variant: id of the variant (new or existing) the set is bound to; the form has novariantIdfield (seeaddProductSetBaseFields()), so this parameter is the only source of truth, never a request value (saveProductVariantForm())
Return values
ProductSetbuildProductSetItems()
Builds the owned `SetItemRel` collection from the composition rows (PK set plus item, `count`).
private
buildProductSetItems(ArrayHash<string|int, mixed> $values, ProductSet $set) : ArrayCollection<int, SetItemRel>
productSet on the child is not set here; linkOwnedCollections fills it from the owner.
A self-reference is dropped here (the Api guard would reject it anyway); other cycles (A contains B contains A) are detected only by the Api, since the admin cannot compute them without querying the DB.
Parameters
- $values : ArrayHash<string|int, mixed>
- $set : ProductSet
Return values
ArrayCollection<int, SetItemRel>buildProductSetM2m()
Builds thin id references for a plain many-to-many collection; the Api `hydrateForPersist` resolves them as managed entities and rewrites the join table.
private
buildProductSetM2m(ArrayHash<string|int, mixed> $values, string $container, string $idField, T> $entityClass) : ArrayCollection<int, T>
Parameters
- $values : ArrayHash<string|int, mixed>
- $container : string
- $idField : string
- $entityClass : T>
Tags
Return values
ArrayCollection<int, T>buildProductSetParameterRows()
Converts the submitted values to flat id/value/value2 rows via {@see ParameterFieldBuilder::extractValues()}.
private
buildProductSetParameterRows(ArrayHash<string|int, mixed> $values) : array<int, array{id: int, value: string, value2?: string}>
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
array<int, array{id: int, value: string, value2?: string}>buildProductSetRelationRowDefaults()
Returns the multiplier prefill rows of all relation collections from the loaded set.
private
buildProductSetRelationRowDefaults(ProductSet $set) : array<string, array<int, array<string, mixed>>>
Parameters
- $set : ProductSet
Return values
array<string, array<int, array<string, mixed>>>buildVariantModeAfterSave()
Returns the post-save hook for mode `product-variant`, which does no page redirect (the modal is a dedicated shell above the presenter page): - `save` (AJAX): closes the modal (`payload->modalClose`); - `update` (AJAX): keeps the modal open and redraws the whole component (fresh `$editedSet`, see {@see saveProductVariantForm()}); always also redraws the variant list and flash on the presenter; - non-AJAX fallback (JS disabled): a full PRG to the product Variants tab ({@see prgAnchor()}).
private
buildVariantModeAfterSave() : callable
Return values
callablecleanupImageUploader()
Deletes the dropzone temp folder after a successful save.
private
cleanupImageUploader() : void
Call from saveProductSetForm() / saveProductVariantForm() after a successful productSetManager->save().
collectProductSetImageMeta()
Builds `$meta['images']`, the sorted gallery rows (unchanged Api contract, see the trait docblock).
private
collectProductSetImageMeta(ArrayHash<string|int, mixed> $values) : array<int, array{assetId: int, image?: array{name: string, data?: string, path?: string}}>
The dropzone order comes first (uploaderItems: existing assetId and new uploads as a path
reference, global order from the JS reindexing), then the classic fallback (new files from
imagesClassic, appended at the end because the classic input carries no existing images or order).
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
array<int, array{assetId: int, image?: array{name: string, data?: string, path?: string}}>configureImageUploader()
Configures the dropzone uploader; called from the component factory on every request, also on the upload/remove AJAX signals where the rest of the form is not built. Existing images come only from the loaded set (edit); a new set gets an empty dropzone.
private
configureImageUploader(UploaderDropzone $uploader) : void
Parameters
- $uploader : UploaderDropzone
entityReference()
Returns a light reference to an entity (id only) that the Api hydrator resolves as a managed entity.
private
entityReference(T> $entityClass, int|null $id) : T|null
Named differently to avoid clashing with ProductFormTrait::referenceOrNull().
Parameters
- $entityClass : T>
- $id : int|null
Tags
Return values
T|nullextractSubmittedParameterIds()
Returns the deduplicated positive IDs from the submitted `parameterRows[*][parameterId]`; non-row multiplier keys are skipped.
private
extractSubmittedParameterIds() : array<int, int>
Return values
array<int, int>flashRejectedImages()
Flashes gallery images that the Api rejected (type, dimensions, unreadable); the save went through without them. Uses the presenter channel (`flashMessagesSnippet` is redrawn after a save in both modes).
private
flashRejectedImages(Result $result) : void
Parameters
- $result : Result
getDefaultUnitId()
Returns the default unit for a new product: the first catalog unit by order.
private
getDefaultUnitId() : int|null
Return values
int|nullgetFieldBuilder()
private
getFieldBuilder() : ParameterFieldBuilder
Return values
ParameterFieldBuildergetProductSetParameterValues()
Returns the prefill values (`parameterRels`) shaped by parameter type.
private
getProductSetParameterValues() : array<int, mixed>
Uses the edited set, otherwise the duplicate source set (same priority as resolveDuplicateParameters()); category-seeded parameters carry no values.
Return values
array<int, mixed>getUnitOptions()
Returns the unit options for the select: `unit_id => "pcs (piece)"`. Only active items: a deactivated unit is not offered for a new choice, but products that already have it keep it.
private
getUnitOptions() : array<int, NotTranslate>
The text is built from DB data (code and unit name), not a key, so it uses NotTranslate;
otherwise the form domain translator looks it up as a key and Tracy reports a missing translation.
Return values
array<int, NotTranslate>guardRelationRowLimit()
Guards the row limit of one of the five relation collections, measured on the raw HTTP data in `onAnchor` (see {@see MultiplierLimits::guard()} for why `onAnchor`).
private
guardRelationRowLimit(Form $form, string $key) : void
The main editForm (standalone and bundle mode) does not render $form->getErrors(), so
the error is also flashed on the presenter; in variant mode editForm is AJAX and its
onError (see FormTrait) already redraws flashMessagesSnippet, so this channel
covers both modes.
Parameters
- $form : Form
- $key : string
isProcessableImageData()
Returns whether GD can process the base64 image (non-fatal validation).
private
isProcessableImageData(string $base64) : bool
It uses the same library as the Api afterSave (imagecreatefromstring), so what passes here
passes there. An empty, invalid or unprocessable image returns false and
collectProductSetImageMeta() skips it instead of failing the whole variant save.
Parameters
- $base64 : string
Return values
boolisProcessableImageFile()
Same check for a file from the dropzone temp folder. Only the `path` reference goes to the Api; the content is read one file at a time and released immediately.
private
isProcessableImageFile(string $path) : bool
Parameters
- $path : string
Return values
boolloadEshopParameterGroups()
private
loadEshopParameterGroups(array<string|int, ParameterFormItem> $items) : array<string|int, ParameterGroupInterface>
Parameters
- $items : array<string|int, ParameterFormItem>
Return values
array<string|int, ParameterGroupInterface>loadEshopParameters()
Loads the active root parameters for the given IDs, without rule graphs.
private
loadEshopParameters(array<int, int> $ids) : array<string|int, ParameterFormItem>
IMPORTANT: rules (dependent selects) are never applied in the eshop: the admin controls the
set's parameter list fully and the front end always shows all choices. optionsCollection
stays because it holds select choices, not rules. The required flag of a parameter applies to the
parameters the admin put on the set.
Parameters
- $ids : array<int, int>
Return values
array<string|int, ParameterFormItem>loadProductSetSelectData()
Loads the form axes (currencies, groups, regions, VAT, states) once per request, before the fields are built.
private
loadProductSetSelectData() : void
parameterDisplayName()
private
parameterDisplayName(Parameter $parameter) : string
Parameters
- $parameter : Parameter
Return values
stringprepareExistingAssets()
Returns the existing set images for the dropzone, sorted by `order` (`order === 1` is the main photo, see {@see \App\Core\Base\Shared\Eshop\Models\Entities\ProductSetImageRel::isMain()}).
private
prepareExistingAssets(ProductSet $set) : array<int, array{imageId: int, thumbPath: string, isMain: bool, order: int}>
Parameters
- $set : ProductSet
Return values
array<int, array{imageId: int, thumbPath: string, isMain: bool, order: int}>prepareParameterForm()
Loads the active parameters (by multiplier rows or the edited set), their groups and the context, and initializes the builder.
private
prepareParameterForm() : void
Idempotent.
prgAnchor()
Returns the anchor for a non-AJAX PRG redirect (snapshot delete, "create card" etc.).
private
prgAnchor([string $standaloneAnchor = 'this#set-tab-form' ]) : string
Standalone/bundle: the anchor of the own tab on this page (#set-tab-*). Mode product-variant:
that anchor would make no sense in the modal (it does not reopen after a full navigation), so the
redirect goes to the product Variants tab. Limitation: a non-AJAX action in the modal therefore
closes it (the whole PRG is a new page); rare (JS off), data stays consistent.
Parameters
- $standaloneAnchor : string = 'this#set-tab-form'
Return values
stringredrawVariantModal()
Redraws the host snippet `setEditHost` on an AJAX submit error (validation or save failure).
private
redrawVariantModal() : void
The component renders again with the same submitted form (inline field errors) and flash, and the modal stays open. Does nothing on a non-AJAX request (full page re-render, rare, JS off).
redrawWizardStep()
Redraws one wizard step: "add/remove tier" in step 2 (price matrix, `$onChange` of {@see PriceSnapshotTrait::buildPriceTierMatrix()}) invalidates only the snippet `wizardPriceStep`, not the whole component, so fields typed in other steps stay untouched in the DOM.
private
redrawWizardStep(string $snippet) : void
Parameters
- $snippet : string
reloadEditedSet()
Reloads `$this->editedSet` through the canonical `EDIT_INCLUDES`.
private
reloadEditedSet([int|null $setId = null ]) : void
Needed after an action that changed state outside the main object-graph save (for example
creating the stock card maps variant.stockItem through the Api bridge), because editedSet
is resolved in the component constructor and would be stale. Without the reload the modal
re-render would show the old state.
Parameters
- $setId : int|null = null
resolveActiveParameterIds()
Returns the IDs of parameters whose typed field is built.
private
resolveActiveParameterIds() : array<int, int>
On GET: the edited set's parameters with a value, otherwise resolveDuplicateParameters(). On POST: the submitted multiplier rows, which always win over the seed. Bundle mode and standalone add prefill nothing.
Return values
array<int, int>resolveDuplicateParameters()
Returns parameters for prefilling a new variant opened in a duplicate context (`$duplicateFromSetId`).
private
resolveDuplicateParameters() : array<int, Parameter>
The distinct parameters of the source set win over the category prefill from resolveProductCategoryParameters(); without a source set it falls back to the category prefill.
Return values
array<int, Parameter>resolveEditedSetParameters()
Returns the distinct parameters of a set (by `parameterRels`) sorted by `lft`, so the row order is stable.
private
resolveEditedSetParameters([ProductSet|null $set = null ]) : array<int, Parameter>
Parameters come from the rels and carry translations (includes parameterRels.parameter).
$set defaults to $this->editedSet; resolveDuplicateParameters() passes the
duplicate source set.
Parameters
- $set : ProductSet|null = null
Return values
array<int, Parameter>resolveParameterLabel()
private
resolveParameterLabel(int $id) : string|null
Parameters
- $id : int
Return values
string|nullresolveProductCategoryParameters()
Returns parameters for prefilling a new variant: the Api bridge `parameters-for-product` aggregates over all product categories (distinct, ordered by minimum `order`).
private
resolveProductCategoryParameters() : array<int, Parameter>
Resolved as entities so buildParameterRowDefaults() gets the row label.
Bundle mode and standalone add prefill nothing ($this->productId is null there).
Return values
array<int, Parameter>resolveUploaderToken()
Returns the dropzone temp folder token: null on a fresh GET (the uploader generates one in `configureImageUploader()`), on a POST (submit or validation error) it is read from the hidden field so the same temp folder is reused.
private
resolveUploaderToken() : string|null
Return values
string|nullsaveProductSetForm()
private
saveProductSetForm(ArrayHash<string|int, mixed> $values[, Form|null $form = null ]) : bool
Parameters
- $values : ArrayHash<string|int, mixed>
- $form : Form|null = null
Return values
boolsaveProductVariantForm()
Saves mode `product-variant`: two entities through their own CRUD stack, in this order.
private
saveProductVariantForm(ArrayHash<string|int, mixed> $values) : bool
- The variant: a new one (
$this->variantId0/null) is created, an existing one updated. - The set (type=1): an object-graph save (see
buildProductSetEntity()) referencing the variant (possibly newly created).
IMPORTANT: $meta['stockItemId'] is sent to productVariantManager->save() only when a real
value exists (practically never, see $stockItemId below). If the key were sent as null on
every edit save, ProductVariantManager::save() would read it as "detach the card" and unmap the
existing stock card (FK to null) on every save: stockItem is #[Transient] on the Admin leaf,
so the object-graph save never changes the FK, and meta['stockItemId'] is the only channel
that does. The card is created exclusively through foundStockItemCard() (a new variant below
after a successful set save; a legacy variant without a card through
StockOperationTrait::ensureStockItemCard()).
After creating a new variant it switches the host active key (ProductPresenter::$activeSetEditVariantId,
persistent) to the new id and forces a redraw of the presenter setEditHost snippet. The
Multiplier key the modal points to was fixed in the DOM before the first save (the hidden _do
field carries setEdit-0-editForm-submit); without a redraw to the correct key the next "Save
and continue editing" in the same modal would create another new variant instead of updating the
one just created.
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
boolsetProductSetFormDefaults()
private
setProductSetFormDefaults(Form $form) : void
Parameters
- $form : Form
suggestParameters()
Suggests root parameters by name, skipping the already picked ones on POST: the typed `parameter_X` field exists once per parameter.
private
suggestParameters(string $query) : array<int, array{id: int, label: string}>
Parameters
- $query : string
Return values
array<int, array{id: int, label: string}>unitName()
Returns the unit name in the current language, or the first non-empty translation when missing.
private
unitName(Unit $unit) : string
Parameters
- $unit : Unit
Return values
stringvariantBelongsToContextProduct()
Returns whether the variant `$variantId` (deleted or not) belongs to the context product `$productId`.
private
variantBelongsToContextProduct(int $variantId) : bool
Parameters
- $variantId : int
Return values
boolwireProductSetMultiplierButtons()
Wires the add/remove buttons of a multiplier: UI-only actions (naja redraws just its snippet), hence `setValidationScope([])`; otherwise removing a row would raise required-field errors elsewhere.
private
wireProductSetMultiplierButtons(Multiplier $multiplier, string $snippet) : void
Parameters
- $multiplier : Multiplier
- $snippet : string