CoolCMS API

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:

  1. The object graph (api/eshop/product-set): scalars, texts, set composition, accessories and four many-to-many collections. Owned collections have orphanRemoval, so they are sent complete; prices and vats are in the Api defaultSkipFields and the graph save never touches them.
  2. $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

Methods

createComponentEditForm()

protected createComponentEditForm() : Form
Return values
Form

parameterRowPicker()

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
PickerConfig

resolveSupplyKindDefaultId()

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|null

addParameterRowFields()

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>

buildParameterRowDefaults()

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 no variantId field (see addProductSetBaseFields()), so this parameter is the only source of truth, never a request value (saveProductVariantForm())

Return values
ProductSet

buildProductSetItems()

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
template
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
callable

cleanupImageUploader()

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
template
Return values
T|null

extractSubmittedParameterIds()

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|null

getProductSetParameterValues()

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
bool

isProcessableImageFile()

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
bool

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

prepareExistingAssets()

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
string

redrawVariantModal()

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|null

resolveProductCategoryParameters()

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|null

saveProductSetForm()

private saveProductSetForm(ArrayHash<string|int, mixed> $values[, Form|null $form = null ]) : bool
Parameters
$values : ArrayHash<string|int, mixed>
$form : Form|null = null
Return values
bool

saveProductVariantForm()

Saves mode `product-variant`: two entities through their own CRUD stack, in this order.

private saveProductVariantForm(ArrayHash<string|int, mixed> $values) : bool
  1. The variant: a new one ($this->variantId 0/null) is created, an existing one updated.
  2. 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
bool

setProductSetFormDefaults()

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
string

variantBelongsToContextProduct()

Returns whether the variant `$variantId` (deleted or not) belongs to the context product `$productId`.

private variantBelongsToContextProduct(int $variantId) : bool
Parameters
$variantId : int
Return values
bool

wireProductSetMultiplierButtons()

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
On this page

Search results