CoolCMS API

ProductSetEdit extends BaseComponent
in package
uses AdminComponentFormTrait, ComponentPickerHostTrait, FormTrait, PickerTrait, StockOperationTrait, FormHelpersTrait, ModalAwareTrait, PriceSnapshotTrait, FeedFieldsTrait

FinalYes

Edits or adds a product set; hosted by `ProductSetPresenter` and `ProductPresenter`.

Modes:

  • standalone: safe fallback with the product, variant and type pickers (no presenter uses it now);
  • product-variant: modal in ProductPresenter without those pickers and without set composition, with a stock tab whose stock item card is always created automatically;
  • bundle: ProductSetPresenter, type fixed to BUNDLE, set composition is the first section.

The price list tab (own AJAX snapshot bridge, PriceSnapshotTrait) and the comment and rating tabs live here; the shared presenter ModalWindow is reached through ModalAwareTrait. The VAT rate is decided by TaxManager from the supply kind of the set, not by an admin-entered history.

Table of Contents

Constants

EDIT_INCLUDES  : array<int, string> = [ 'translations', 'product.translations', 'prod...
Canonical includes list for loading the edited set; the single source of truth, also read by `ProductSetPresenter::actionEdit()`. Every object-graph save of a set needs the full includes, otherwise orphan removal deletes the missing relations.
PRIVILEGE_STOCK_CARD  : mixed = ':Admin:Store:StockItem:add'
Creating the missing stock item card of a variant writes to the warehouse (`:Api:Eshop:ProductVariant:foundStockItemCard`).
PRIVILEGE_STOCK_MOVE  : mixed = ':Admin:Store:StockDocument:add'
Stock actions of the variant editor (receipt, transfer) write a stock document, so they check the same admin privilege as the Store module. The Api enforces `:Api:Store:StockDocument:receive` / `transfer` independently; this is its admin counterpart, not a replacement.

Properties

$container  : Container
$onStockChanged  : array<int, callable(int): void>
$routerDispatcher  : RouterDispatcher
$translator  : ITranslator
$appContext  : AppContext
$componentParameters  : array<string, mixed>
$currencyContext  : CurrencyContext|null
$deviceDetector  : DeviceDetector|null
$languageContext  : LanguageContext|null
$localizationContext  : LocalizationContext|null
$settingContext  : SettingContext
$themeContext  : ThemeContext|null
$translatorDomain  : string
$adminLanguageManager  : LanguageManager
$apiBridge  : ApiBridge
$commentThreadFactory  : CommentThreadFactory
$contextProduct  : Product|null
Edited product in the `product-variant` mode, loaded lazily by {@see loadContextProduct()}.
$contextProductLoaded  : bool
$currencyManager  : CurrencyManager
$currencyOptions  : array<int, string|NotTranslate>
$duplicateFromSetId  : int|null
$duplicateSourceSet  : ProductSet|null
Source selling set of a duplication, loaded lazily by {@see loadDuplicateSourceSet()}.
$duplicateSourceSetLoaded  : bool
$editedSet  : ProductSet|null
Edited set (null when adding), set by {@see resolveEditedSet()}.
$editingPriceSnapshotTime  : string|null
Snapshot being edited in the modal (`publishedTime`); null when adding.
$feedManager  : FeedManager
$feedOverrideModesCache  : array<int, string>|null
Memoized value modes of the edited set, `feedParameterId => mode`. Taken from the first row of the (parameter, set) pair, like the generator does.
$feedOverrideRowsCache  : array<int, array<int, string>>|null
Memoized override rows of the edited set, `feedParameterId => list<value>` ordered by `r.id ASC` (the order of repetitions in the XML).
$feedOverrideStructureCache  : array<int, array{feedId: int, feedName: string, tree: list>}>|null
Memoized tab axis: feeds with their parameter trees (including saved repetition counts).
$feedParameterActionLabelsCache  : array<int, string>|null
$feedParameterActionManager  : FeedParameterActionManager
$formActionsFactory  : FormActionsFactory
$formNotices  : array<int, array{type: string, key: string}>
Result-level variant save messages (duplicate code, taken variant, cycle, generic), rendered directly in the modal content (`setEditHost`, `Default.latte`).
$itemExcludeIds  : array<int, int>|null
$langNavFactory  : LangNavFactory
$mode  : string
One of `standalone`, `product-variant`, `bundle`; see the class docblock.
$parameterGroupManager  : ParameterGroupManager
$parameterManager  : ParameterManager
$pickerFactory  : PickerFactory
$preloadedSet  : ProductSet|null
$productCategoryManager  : ProductCategoryManager
$productId  : int|null
$productManager  : ProductManager
$productSetFeeManager  : ProductSetFeeManager
$productSetLinkManager  : ProductSetLinkManager
$productSetManager  : ProductSetManager
$productStateManager  : ProductStateManager
$productStateOptions  : array<int, string|NotTranslate>
$productTagManager  : ProductTagManager
$productVariantManager  : ProductVariantManager
$ratingThreadFactory  : RatingThreadFactory
$setId  : int|null
$snapshotSet  : ProductSet|null
Set with loaded price history, loaded separately from `$editedSet` because the main set form does not load `prices`/`vats` at all (see `EDIT_INCLUDES`). Lazy.
$stockLevelManager  : StockLevelManager
$supplyKindManager  : SupplyKindManager
$supplyKindOptions  : array<int, string>
$unitManager  : UnitManager
$uploaderDropzoneFactory  : UploaderDropzoneFactory
$userGroupManager  : UserGroupManager
$userGroupOptions  : array<int, string|NotTranslate>
$variantId  : int|null
$vatManager  : VatManager
$vatOptions  : array<int, string|NotTranslate>
$vatPercents  : array<int, string>
Raw VAT rate percentages (id => "21.00") for the client-side "price with VAT" calculation in the price list; `vatOptions` holds labels with a percent sign that must not be parsed.
$warehouseManager  : WarehouseManager

Methods

__call()  : mixed
Dispatches `renderXyz()` to `render('Xyz', ...$args)`.
__construct()  : mixed
canMoveStock()  : bool
Returns whether the logged-in admin may move stock from the variant editor.
createComponent()  : IComponent|null
getAppContext()  : AppContext
getComponentParameter()  : mixed
Returns a component parameter, or null when it is not set.
getContainer()  : Container
getCurrencyContext()  : CurrencyContext|null
getLanguageContext()  : LanguageContext|null
getLocalizationContext()  : LocalizationContext|null
getRouterDispatcher()  : RouterDispatcher
getSettingContext()  : SettingContext
getSettings()  : array<int, array<string, mixed>>
getThemeContext()  : ThemeContext|null
handleAddPriceSnapshot()  : void
handleDeletePriceSnapshot()  : void
The link is not AJAX (styled `data-grid-confirm`), so after deleting a redirect (PRG) returns to the tab.
handleEditPriceSnapshot()  : void
handleOpenReceiveModal()  : void
Opens the receipt sub-modal.
handleOpenTransferModal()  : void
Opens the transfer sub-modal.
hasServiceByType()  : bool
Returns whether the container has a service of the given type.
initializeAllContexts()  : void
injectDeviceDetector()  : void
onCreate()  : void
onFeedOverridesFormSuccess()  : void
Saves the Feeds tab, set-scoped so it never touches the overrides of other sets ({@see \App\UI\Api\Eshop\Models\Managers\FeedManager::saveParameterRelsForSet()}).
onPriceSnapshotFormSuccess()  : void
onStockReceiveSuccess()  : void
onStockTransferSuccess()  : void
render()  : void
setAppContext()  : $this
setComponentParameter()  : void
Sets a component parameter.
setCurrencyContext()  : $this
setLanguageContext()  : $this
setLocalizationContext()  : $this
setSettingContext()  : $this
setThemeContext()  : $this
setup()  : void
setupAppContext()  : void
typedComponent()  : T
Returns a subcomponent with a verified type.
createAdminComponentForm()  : Form
createComponentAdminFormActions()  : FormActions
Save and back buttons of the form; own instance, as in `AdminEditFormComponentsTrait`.
createComponentAdminLangNav()  : LangNav
Language tabs of the multilingual fields; own instance, as in `AdminEditFormComponentsTrait`.
createComponentCommentThread()  : CommentThread
Set comment management for the comments tab (editing only).
createComponentEditForm()  : Form
createComponentFeedOverridesForm()  : Form
Builds the sub-form of the Feeds tab: one multiplier `feedMulti_<id>` per parameter (a non-multiple parameter is a multiplier with `maxCopies = 1` and no buttons).
createComponentImgUploader()  : UploaderDropzone
Builds the set gallery dropzone uploader.
createComponentPicker()  : Multiplier<string|int, Picker>
createComponentPriceSnapshotForm()  : Form
Builds the modal form: matrix of tier (`fromAmount`) x user group x currency to price, original price and points.
createComponentRatingThread()  : RatingThread
Set rating management for the ratings tab (editing only).
createComponentStockReceiveForm()  : Form
createComponentStockTransferForm()  : Form
currentLangId()  : int|null
Returns the current language id (for picker captions etc.), or null when unknown or not attached.
frontPresenter()  : BasePresenter|null
Returns the front presenter the component is attached to (null outside the front end or when detached).
getFormLanguages()  : array<int, Language>
Returns the languages for multilingual form fields (admin `LanguageManager`, constructor-injected).
getModalWindow()  : ModalWindow
getTranslator()  : ITranslator
intOrNull()  : int|null
Converts a mixed value to an int or null (an empty or zero value becomes null).
parseDateTime()  : DateTime|null
Parses a `datetime-local` input or text value into a `DateTime`, or null when empty or invalid.
pickers()  : array<string, PickerConfig>
Returns the component pickers; must be pure and idempotent, it is also called on the AJAX signal `suggest!`.
productPicker()  : PickerConfig
Picker of the owning `Product`: a single top-level field outside the multiplier, so no `targetContainer` or `displayField`.
productSetAccessoryPicker()  : PickerConfig
Picker of an accessory in the `accessories` multiplier row (many-to-many `ProductSet` to `ProductSet`).
productSetFeePicker()  : PickerConfig
Picker of a fee in the `fees` multiplier row (many-to-many `ProductSet` to `ProductSetFee`).
productSetItemPicker()  : PickerConfig
Picker of a set composition item: `setItems` row targeting another set (a ready bundle too, i.e. nested sets). Links to `:Admin:Eshop:ProductSet` so the item detail opens from the row.
productSetLinkPicker()  : PickerConfig
Picker of a link in the `links` multiplier row (many-to-many `ProductSet` to `ProductSetLink`).
productSetVariantPicker()  : PickerConfig
Picker of the sold variant: top-level `variantId` of a set with `type = PRODUCT`. The 1:1 relation is guarded by the Api guard and a unique index.
productTagPicker()  : PickerConfig
Picker of a tag in the `tags` multiplier row (many-to-many `ProductSet` to `ProductTag`).
setTemplateFile()  : void
setTranslator()  : self
str()  : string
Converts a mixed form value to a string safely.
strOrNull()  : string|null
Converts a mixed value to a string or null (an empty string is stored as null).
addFeedOverrideModeSelect()  : void
Builds the value mode select: one per root node (children and repetitions of a `multiple` node do not get one).
addFeedOverrideMultiplier()  : void
Builds the multiplier of one node; a copy is one repetition of the XML node.
addInitialStockFields()  : void
Adds the per-warehouse initial stocking fields (quantity, price) and the shared currency.
addPriceGroupCurrencyFields()  : void
Adds the per-currency cells (price, original price, points) for one group; shared so that `group{id}` and the base `groupAll` have identical fields.
addProductSetBaseFields()  : void
Adds the basics: codes, unit, type, owning product, state, publication and goods parameters.
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.
applyUnit()  : void
Writes the catalog choice and, at the same time, the print abbreviation.
buildFeedOverrideDefaults()  : array<string, mixed>
Builds the prefill from {@see \App\UI\Admin\Eshop\Models\Managers\FeedManager::getParameterRelsForSet()} (order `r.id ASC`, the order of repetitions in the XML).
buildFeedParameterRenderTree()  : array<int, array<string, mixed>>
Converts parameter tree nodes into the shape used by the form and the rendering.
buildPriceSnapshotDefaults()  : array{sameForAll: bool, tiers: list>}
Builds the matrix prefill for the edited snapshot: rows of the given `publishedTime` grouped by tier (`fromAmount`), inside by group x currency (matches the multiplier containers).
buildPriceSnapshotModalContent()  : string
Renders the modal content (partial) to a string for `ModalWindow::setContent()`.
buildPriceTierMatrix()  : void
Adds the tier x group x currency matrix to the form: the `$form['tiers']` multiplier with create and remove buttons.
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).
buildProductSetPriceHistory()  : array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>
Returns the read-only price list history for the tab: snapshots grouped by `publishedTime` (newest first) with an `editable` flag.
buildStockReceiveModalContent()  : string
buildStockTransferModalContent()  : string
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()}).
collectFeedOverrideDefaults()  : void
Fills defaults of one node. Children are not reached by recursion: they live in the parent copy.
collectFeedOverrideNodeRows()  : void
Collects the rows of one node (see the trait docblock: why a node with children also gets a parent row and why an empty child is stored as an empty string).
collectFeedOverrideRows()  : array<int, array{feedParameterId: int, value: string, valueMode: string}>
Converts form values into flat bridge rows, in the order the repetitions must appear in the XML (the bridge stores them sequentially and the generator reads them `ORDER BY r.id`).
collectInitialStockRows()  : array<int, array{quantity: float, unitPrice: float|null}>
Collects the initial stocking rows to be sent as receipts.
collectPriceSnapshotRows()  : array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>
Collects the matrix rows to send to the bridge; an empty price means the combination is not stored. The Api manager decides the rest (collisions, guard, denormalized recalculation).
createMultiplierComponent()  : Multiplier<string|int, IComponent>
Creates a multiplier for the component.
createNewComponentInstance()  : IComponent|null
Creates a new component instance without adding it to the presenter; used by the multiplier.
createProductSetLink()  : array{id: int, label: string}|null
Creates a link from the typed URL and returns it to the picker for selection.
createStandardComponent()  : IComponent|null
Creates a standard component (not a multiplier).
currentStockItemId()  : int|null
Stock item id of the edited variant; null when it has no card yet ({@see ensureStockItemCard()} creates it at the first stock action).
currentUserId()  : int|null
Returns the logged-in admin id, stored as `userId` of the stock document.
ensureStockItemCard()  : void
Creates the stock item card of a legacy variant at its first stock action (receipt or transfer); no-op when the card already exists.
entityReference()  : T|null
Returns a light reference to an entity (id only) that the Api hydrator resolves as a managed entity.
extractPriceCell()  : array<string, mixed>|null
Extracts one cell (price, original price, points) from the raw tier array.
extractPriceGroupBase()  : array<string, mixed>
Returns the cells of one representative group of a tier as the base for `groupAll`; empty when no group exists (empty tier).
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.
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.
groupPriceRowsByUserGroup()  : array<int, array{userGroup: string, rows: list>}>
Turns the rows of one snapshot into one table per user group (order of `$this->userGroupOptions`, unknown groups last), sorted by currency x tier inside. The group is the table heading, so rows do not carry it.
httpRequest()  : IRequest
Returns the HTTP request through the presenter; a control has no `getHttpRequest()`.
isPriceTiersSameForAllGroups()  : bool
Returns true when every tier x currency has identical values across all user groups (a missing row is null and must match too).
isSignalRequest()  : bool
Tells whether a signal is being handled; a control has no `getSignal()`, only the presenter has.
isSnapshotEditable()  : bool
Tells whether a snapshot may be edited or deleted: only one dated today or scheduled in the future. `publishedTime` is a lexicographically comparable string.
isWizard()  : bool
Tells whether a new variant is being added: the `product-variant` mode with a new (unsaved) variant, created from scratch or as a duplicate (`$duplicateFromSetId`); in both cases `$variantId` is 0 or null.
loadAvailability()  : array{availableSets: ?int, inStock: bool, deliveryDays: ?int, infoText: ?string, attributeClass: ?string, unmappedVariantIds: list, unresolvedSetIds: list}|null
Loads the stock panel data. `available` is never cached: it is a live query on every render, even within one component instance.
loadContextProduct()  : Product|null
Loads the edited product (outside the set) in the `product-variant` mode, lazily; used for the GET prefill of new variant texts ({@see Traits\FormTrait::setProductSetFormDefaults()}) and for guards. `$productId` is server-side context from `ProductPresenter::createComponentSetEdit()`, never from the form.
loadCurrentPricePanel()  : array<int, array{price: string, originalPrice: string, points: int, fromAmount: int, currencyId: int}>
Loads the current price panel (price list tab) through `PriceResolver` (bridge action), not through the denormalized column.
loadDuplicateSourceSet()  : ProductSet|null
Loads the source selling set of a duplication, lazily; only for a new (unsaved) variant in the `product-variant` mode with the constructor context `$duplicateFromSetId` (supplied server-side by `ProductPresenter::createComponentSetEdit()`, which also ran the variant ownership guard). The GET defaults ({@see Traits\FormTrait::setProductSetFormDefaults()}, {@see Traits\ParameterFieldsTrait}) take scalars, texts, relations and parameters from it instead of the plain product name prefill. The `code`, the gallery (`assetRels`) and the stock item mapping (`stockItemId`) are not read from it, so they are not copied: a duplicate needs a new code and has no images.
loadFeedOverrideModes()  : array<int, string>
Returns the value modes of the edited set, `feedParameterId => mode`. Filled together with the rows ({@see loadFeedOverrideRows()}), it is the same bridge query.
loadFeedOverrideRows()  : array<int, array<int, string>>
Returns the override rows of the edited set across all feeds, `feedParameterId => list<value>`.
loadFeedOverrideStructure()  : array<int, array{feedId: int, feedName: string, tree: list>}>
Returns the feeds with their parameter trees, using one bridge query per feed (memoized).
loadFeedParameterActionLabels()  : array<int, string>
Returns the catalog of parameter actions, `actionId => translated name` (read-only).
loadProductSetSelectData()  : void
Loads the form axes (currencies, groups, regions, VAT, states) once per request, before the fields are built.
loadSnapshotSet()  : ProductSet|null
Returns the set with loaded price history (`includes: ['prices']`). Lazy.
loadStockLevels()  : array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>
Returns the read-only rows of the per-warehouse stock table.
loadWarehouseOptions()  : array<int, NotTranslate>
Returns the active warehouses for a select (id => "code — name"), only `activated=true`.
parsePickerName()  : array{0: string, 1: list}
Splits a picker instance name into the config key and the multiplier row indices; see {@see \App\UI\Admin\System\Presenters\Traits\Pickers\PickerHostTrait::parsePickerName()} for the full explanation of the separator.
prgAnchor()  : string
Returns the anchor for a non-AJAX PRG redirect (snapshot delete, "create card" etc.).
productLabel()  : string
Returns the product name in the current language (first non-empty as fallback).
productSetAncestorIds()  : array<int, int>
Returns the ancestors of a set in the composition tree, i.e. the sets that contain it directly or indirectly.
productSetFeeLabel()  : string
Returns the fee name in the current language (first non-empty as fallback).
productSetItemExcludeIds()  : array<int, int>
Returns the sets the composition picker must not offer: the edited set itself and all its ancestors (a set cannot contain itself). The authoritative guard is in the Api manager; this is UI prevention.
productSetLabel()  : string
Returns `<code> — <name>` in the current language (falls back to whichever part is non-empty).
productSetLinkLabel()  : string
Returns the link label: its URL in the current language (first non-empty address as fallback).
productTagLabel()  : string
Returns the tag name in the current language (first non-empty as fallback).
productVariantLabelsOf()  : array<int, string>
Returns the labels of all variants of a product: `variantId => "<product name> (#<variant id>)"`.
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`.
requireStockMovePrivilege()  : void
Enforces the stock privileges, one set for opening the sub-modal and for its submit.
resolveEditedSet()  : ProductSet|null
Returns `preloadedSet` (the presenter already has it from `actionEdit()`), otherwise loads the set by `setId`.
resolvePriceVatPercents()  : string|null
Returns the VAT rate for the client-side "price with VAT" computing when the form has no VAT select (the "Price list" edit modal); null when the rate is unknown (no seller or supply kind), then no preview is shown.
resolveProductLabel()  : string|null
resolveProductSetAccessoryLabel()  : string|null
resolveProductSetFeeLabel()  : string|null
resolveProductSetLinkLabel()  : string|null
resolveProductTagLabel()  : string|null
resolveProductVariantLabel()  : string|null
resolveSubmittedSnapshotTime()  : string|null
Returns the validity time from the form for the Api (`resolvePublishedTime()` takes the string as Europe/Prague).
saveProductSetForm()  : bool
saveProductVariantForm()  : bool
Saves mode `product-variant`: two entities through their own CRUD stack, in this order.
setPriceSnapshotFormDefaults()  : void
Prefills the form when editing; a new snapshot (`editingPriceSnapshotTime === null`) stays empty.
setProductSetFormDefaults()  : void
showPriceSnapshotModal()  : void
Fills the shared admin `ModalWindow` with the current form state and opens it.
showReceiveModal()  : void
showTransferModal()  : void
snapshotErrorKey()  : string
Maps a bridge response code (`OK`, `IMMUTABLE`, `TIME_COLLISION`, `NOT_FOUND`, `DENORM`, `VALIDATION`) to a message key.
suggestProducts()  : array<int, array{id: int, label: string}>
Suggests products by a `like` match on the product name.
suggestProductSetAccessories()  : array<int, array{id: int, label: string}>
Suggests sets by a `like` match on the set name, optionally without the edited set (a set cannot be its own accessory).
suggestProductSetFees()  : array<int, array{id: int, label: string}>
Suggests fees by a `like` match on the fee name.
suggestProductSetItems()  : array<int, array{id: int, label: string}>
Suggests composition items by a `like` match on the set name, excluding `$excludeIds`.
suggestProductSetLinks()  : array<int, array{id: int, label: string}>
Suggests links by a `like` match on the URL (`translations.value`) only, not on the name: names are often identical or missing, so only the address tells links apart.
suggestProductTags()  : array<int, array{id: int, label: string}>
Suggests tags by a `like` match on the tag name.
suggestProductVariants()  : array<int, array{id: int, label: string}>
Suggests variants by a `like` match on the owning product name (a variant has no text of its own).
toDateTimeLocalValue()  : string
Converts `Y-m-d H:i:s` (ORM string, UTC) to a `datetime-local` value in Prague time.
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`.
warehouseLabel()  : string
Returns `code — name` (name in the current language, falling back to the first non-empty one).

Constants

EDIT_INCLUDES

Canonical includes list for loading the edited set; the single source of truth, also read by `ProductSetPresenter::actionEdit()`. Every object-graph save of a set needs the full includes, otherwise orphan removal deletes the missing relations.

public array<int, string> EDIT_INCLUDES = [ 'translations', 'product.translations', 'productState', 'vat', 'asset', // `variant.stockItem` is read-only (stock tab, link to Store): it is `#[Transient]` on // the admin `ProductVariant`, so the save-wipe guard strips it from hydration and // including it only for display is safe. 'variant.stockItem', 'setItems.childSet.translations', 'accessories.accessorySet.translations', 'assetRels.asset', // IMPORTANT: the parameter layer must be in the includes of every object-graph save of // a set, otherwise orphan removal deletes the stored values. `.translations` gives the // parameter name for the row label (picker `parameterName`) on GET; without it the // parameters tab shows `#id` instead of the name. 'parameterRels.parameter.translations', 'tags.translations', 'fees.translations', 'links.translations', 'commentThread', 'ratingThread', ]

PRIVILEGE_STOCK_CARD

Creating the missing stock item card of a variant writes to the warehouse (`:Api:Eshop:ProductVariant:foundStockItemCard`).

private mixed PRIVILEGE_STOCK_CARD = ':Admin:Store:StockItem:add'

PRIVILEGE_STOCK_MOVE

Stock actions of the variant editor (receipt, transfer) write a stock document, so they check the same admin privilege as the Store module. The Api enforces `:Api:Store:StockDocument:receive` / `transfer` independently; this is its admin counterpart, not a replacement.

private mixed PRIVILEGE_STOCK_MOVE = ':Admin:Store:StockDocument:add'

Properties

$onStockChanged

public array<int, callable(int): void> $onStockChanged = []

event fired after a stock change (receipt, transfer); ProductPresenter redraws the variant list (available pieces) on it

$componentParameters

protected array<string, mixed> $componentParameters = []

component parameters set by setComponentParameter()

$translatorDomain

protected string $translatorDomain = 'admin.eshop.presenters.productSet'

$contextProduct

Edited product in the `product-variant` mode, loaded lazily by {@see loadContextProduct()}.

private Product|null $contextProduct = null

$contextProductLoaded caches a null result too, so the query is not repeated.

$contextProductLoaded

private bool $contextProductLoaded = false

$currencyOptions

private array<int, string|NotTranslate> $currencyOptions = []

currencies for the select (id => shortcut; a DB value is wrapped in NotTranslate)

$duplicateFromSetId read-only

private int|null $duplicateFromSetId = null

$duplicateSourceSet

Source selling set of a duplication, loaded lazily by {@see loadDuplicateSourceSet()}.

private ProductSet|null $duplicateSourceSet = null

$duplicateSourceSetLoaded caches a null result too.

$duplicateSourceSetLoaded

private bool $duplicateSourceSetLoaded = false

$editingPriceSnapshotTime

Snapshot being edited in the modal (`publishedTime`); null when adding.

private string|null $editingPriceSnapshotTime = null

$feedOverrideModesCache

Memoized value modes of the edited set, `feedParameterId => mode`. Taken from the first row of the (parameter, set) pair, like the generator does.

private array<int, string>|null $feedOverrideModesCache = null

$feedOverrideRowsCache

Memoized override rows of the edited set, `feedParameterId => list<value>` ordered by `r.id ASC` (the order of repetitions in the XML).

private array<int, array<int, string>>|null $feedOverrideRowsCache = null

$feedOverrideStructureCache

Memoized tab axis: feeds with their parameter trees (including saved repetition counts).

private array<int, array{feedId: int, feedName: string, tree: list>}>|null $feedOverrideStructureCache = null

$feedParameterActionLabelsCache

private array<int, string>|null $feedParameterActionLabelsCache = null

memoized action catalog, actionId => translated name.

$formNotices

Result-level variant save messages (duplicate code, taken variant, cycle, generic), rendered directly in the modal content (`setEditHost`, `Default.latte`).

private array<int, array{type: string, key: string}> $formNotices = []

The presenter flashMessage() is invisible in a component template and the admin flashMessagesSnippet does not exist on the edit page, so a flash would never show. These errors are not bound to a field, so {inputError} does not show them either. Per-request buffer; the component is rebuilt on every request, so old messages never reappear when the modal is reopened.

$itemExcludeIds

private array<int, int>|null $itemExcludeIds = null

memoized exclusion for the composition picker (set and its ancestors), see productSetItemExcludeIds()

$mode read-only

One of `standalone`, `product-variant`, `bundle`; see the class docblock.

private string $mode = 'standalone'

$productStateOptions

private array<int, string|NotTranslate> $productStateOptions = []

marketing states for the select (id => name)

$snapshotSet

Set with loaded price history, loaded separately from `$editedSet` because the main set form does not load `prices`/`vats` at all (see `EDIT_INCLUDES`). Lazy.

private ProductSet|null $snapshotSet = null

$supplyKindOptions

private array<int, string> $supplyKindOptions = []

supply kinds for the select (id => name from the DB; the select has the translator off)

$userGroupOptions

private array<int, string|NotTranslate> $userGroupOptions = []

user groups for the select (id => name)

$vatOptions

private array<int, string|NotTranslate> $vatOptions = []

VAT rates for the select (id => label with percent sign)

$vatPercents

Raw VAT rate percentages (id => "21.00") for the client-side "price with VAT" calculation in the price list; `vatOptions` holds labels with a percent sign that must not be parsed.

private array<int, string> $vatPercents = []

Methods

__call()

Dispatches `renderXyz()` to `render('Xyz', ...$args)`.

public __call(string $name, array<string|int, mixed> $args) : mixed

Called only when the component has no renderXyz() method of its own.

Parameters
$name : string
$args : array<string|int, mixed>

__construct()

public __construct(ProductSetManager $productSetManager, ProductManager $productManager, ProductVariantManager $productVariantManager, ProductTagManager $productTagManager, ProductSetFeeManager $productSetFeeManager, ProductSetLinkManager $productSetLinkManager, ProductStateManager $productStateManager, UnitManager $unitManager, CurrencyManager $currencyManager, UserGroupManager $userGroupManager, VatManager $vatManager, ParameterManager $parameterManager, ParameterGroupManager $parameterGroupManager, ProductCategoryManager $productCategoryManager, PickerFactory $pickerFactory, UploaderDropzoneFactory $uploaderDropzoneFactory, LangNavFactory $langNavFactory, FormActionsFactory $formActionsFactory, LanguageManager $adminLanguageManager, CommentThreadFactory $commentThreadFactory, RatingThreadFactory $ratingThreadFactory, ApiBridge $apiBridge, WarehouseManager $warehouseManager, StockLevelManager $stockLevelManager, FeedManager $feedManager, FeedParameterActionManager $feedParameterActionManager, SupplyKindManager $supplyKindManager, int|null $setId[, string $mode = 'standalone' ][, ProductSet|null $preloadedSet = null ][, int|null $productId = null ][, int|null $variantId = null ][, int|null $duplicateFromSetId = null ]) : mixed
Parameters
$productSetManager : ProductSetManager
$productManager : ProductManager
$productVariantManager : ProductVariantManager
$productTagManager : ProductTagManager
$productSetFeeManager : ProductSetFeeManager
$productSetLinkManager : ProductSetLinkManager
$productStateManager : ProductStateManager
$unitManager : UnitManager
$currencyManager : CurrencyManager
$userGroupManager : UserGroupManager
$vatManager : VatManager
$parameterManager : ParameterManager
$parameterGroupManager : ParameterGroupManager
$productCategoryManager : ProductCategoryManager
$pickerFactory : PickerFactory
$uploaderDropzoneFactory : UploaderDropzoneFactory
$langNavFactory : LangNavFactory
$formActionsFactory : FormActionsFactory
$adminLanguageManager : LanguageManager
$commentThreadFactory : CommentThreadFactory
$ratingThreadFactory : RatingThreadFactory
$apiBridge : ApiBridge
$warehouseManager : WarehouseManager
$stockLevelManager : StockLevelManager
$feedManager : FeedManager
$feedParameterActionManager : FeedParameterActionManager
$supplyKindManager : SupplyKindManager
$setId : int|null
$mode : string = 'standalone'
$preloadedSet : ProductSet|null = null
$productId : int|null = null
$variantId : int|null = null
$duplicateFromSetId : int|null = null

canMoveStock()

Returns whether the logged-in admin may move stock from the variant editor.

public canMoveStock() : bool

IMPORTANT: one policy for all three places (button visibility, the server-side signal gate, the wizard initial stocking). Without it the UI offers receipt and transfer to anyone who may edit the variant, a click tries to create the stock item card, and only the Api then rejects the operation with a generic error. Public because the template calls it.

Return values
bool

createComponent()

public createComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

getComponentParameter()

Returns a component parameter, or null when it is not set.

public getComponentParameter(string $name) : mixed
Parameters
$name : string

getContainer()

public getContainer() : Container
Return values
Container

getSettings()

public getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>

handleDeletePriceSnapshot()

The link is not AJAX (styled `data-grid-confirm`), so after deleting a redirect (PRG) returns to the tab.

public handleDeletePriceSnapshot(string $time) : void
Parameters
$time : string
Attributes
#[Requires]
$methods: 'POST'

handleEditPriceSnapshot()

public handleEditPriceSnapshot(string $time) : void
Parameters
$time : string

handleOpenReceiveModal()

Opens the receipt sub-modal.

public handleOpenReceiveModal() : void

A legacy variant without a card gets it here (ensureStockItemCard()) before the noStockItem guard is evaluated. The AJAX modal never redirects, so a failed guard just stays in place with a message.

handleOpenTransferModal()

Opens the transfer sub-modal.

public handleOpenTransferModal() : void

Creates a missing card like handleOpenReceiveModal(). Guards: the stock item card (nothing to transfer without it) and at least 2 active warehouses (nowhere to transfer to).

hasServiceByType()

Returns whether the container has a service of the given type.

public hasServiceByType(string $className) : bool

The name is composed dynamically (module plus component), so class_exists() runs first, and a true result guarantees a valid type.

Parameters
$className : string
Tags
phpstan-assert-if-true

class-string $className

Return values
bool

onFeedOverridesFormSuccess()

Saves the Feeds tab, set-scoped so it never touches the overrides of other sets ({@see \App\UI\Api\Eshop\Models\Managers\FeedManager::saveParameterRelsForSet()}).

public onFeedOverridesFormSuccess(Form $form) : void

It never calls redirect() on success (in mode product-variant the PRG would close the variant modal); it only adds a notice and redraws the section, like stock operations do.

Parameters
$form : Form

onPriceSnapshotFormSuccess()

public onPriceSnapshotFormSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
$form : Form
$values : ArrayHash<string|int, mixed>

onStockReceiveSuccess()

public onStockReceiveSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
$form : Form
$values : ArrayHash<string|int, mixed>

onStockTransferSuccess()

public onStockTransferSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
$form : Form
$values : ArrayHash<string|int, mixed>

setComponentParameter()

Sets a component parameter.

public setComponentParameter(string $name, mixed $value) : void
Parameters
$name : string
$value : mixed

typedComponent()

Returns a subcomponent with a verified type.

public typedComponent(string $name, T> $type) : T

Use it instead of an inline @var above $this['x'] (which does not narrow the type) and instead of the generic IComponent that $this['x'] returns. See Form::typedComponent().

Parameters
$name : string
$type : T>
Tags
template
Return values
T

createAdminComponentForm()

protected createAdminComponentForm(callable(ArrayHash<string|int, mixed>, Form=): bool $processData[, callable(string): void|null $afterSave = null ]) : Form
Parameters
$processData : callable(ArrayHash<string|int, mixed>, Form=): bool

Saves the data; returns success (handles its own flash).

$afterSave : callable(string): void|null = null

When set, replaces the default backKey/redirect logic after a successful $processData; the parameter is the clicked submit name (update or save). The hidden backKey stays in the form but is unused. For a component hosted in a modal shell, where a page redirect makes no sense (the modal is an AJAX shell over the presenter page).

Return values
Form

createComponentAdminFormActions()

Save and back buttons of the form; own instance, as in `AdminEditFormComponentsTrait`.

protected createComponentAdminFormActions() : FormActions
Return values
FormActions

createComponentAdminLangNav()

Language tabs of the multilingual fields; own instance, as in `AdminEditFormComponentsTrait`.

protected createComponentAdminLangNav() : LangNav
Return values
LangNav

createComponentEditForm()

protected createComponentEditForm() : Form
Return values
Form

createComponentFeedOverridesForm()

Builds the sub-form of the Feeds tab: one multiplier `feedMulti_<id>` per parameter (a non-multiple parameter is a multiplier with `maxCopies = 1` and no buttons).

protected createComponentFeedOverridesForm() : Form

IMPORTANT: defaults are filled unconditionally, with no isMethod('GET') guard like in editForm. Form::setDefaults() is a no-op on a submitted form, so the multiplier builds its copies from the submitted data and the prefill does not interfere. A guard would render the tab with empty fields on any other signal (opening the price list modal, a stock operation calling redrawVariantModal()).

Return values
Form

createComponentPriceSnapshotForm()

Builds the modal form: matrix of tier (`fromAmount`) x user group x currency to price, original price and points.

protected createComponentPriceSnapshotForm() : Form

Switching tiers and groups is client-side only (JS hides and shows), so all fields stay in the DOM; the server always receives the full matrix and filters out empty cells itself (collectPriceSnapshotRows()).

Return values
Form

currentLangId()

Returns the current language id (for picker captions etc.), or null when unknown or not attached.

protected currentLangId() : int|null
Return values
int|null

frontPresenter()

Returns the front presenter the component is attached to (null outside the front end or when detached).

protected frontPresenter() : BasePresenter|null
Return values
BasePresenter|null

getFormLanguages()

Returns the languages for multilingual form fields (admin `LanguageManager`, constructor-injected).

protected getFormLanguages() : array<int, Language>
Return values
array<int, Language>

getTranslator()

protected getTranslator() : ITranslator
Return values
ITranslator

intOrNull()

Converts a mixed value to an int or null (an empty or zero value becomes null).

protected intOrNull(mixed $value) : int|null
Parameters
$value : mixed
Return values
int|null

parseDateTime()

Parses a `datetime-local` input or text value into a `DateTime`, or null when empty or invalid.

protected parseDateTime(mixed $value) : DateTime|null
Parameters
$value : mixed
Return values
DateTime|null

pickers()

Returns the component pickers; must be pure and idempotent, it is also called on the AJAX signal `suggest!`.

protected pickers() : array<string, PickerConfig>

The product-variant mode has no productId, variantId, childSetId (product and variant come server-side from the constructor context and set composition is not available there) and no stockItemId (the stock item card is always created automatically, see StockOperationTrait). The bundle mode has no productId and variantId (a bundle has no product or variant of its own), but has childSetId (set composition is its core), shared with standalone.

Return values
array<string, PickerConfig>

productPicker()

Picker of the owning `Product`: a single top-level field outside the multiplier, so no `targetContainer` or `displayField`.

protected productPicker([string $formName = 'editForm' ][, string $targetField = 'productId' ]) : PickerConfig
Parameters
$formName : string = 'editForm'
$targetField : string = 'productId'
Return values
PickerConfig

productSetAccessoryPicker()

Picker of an accessory in the `accessories` multiplier row (many-to-many `ProductSet` to `ProductSet`).

protected productSetAccessoryPicker([string $container = 'accessories' ][, string $targetField = 'accessorySetId' ][, string $displayField = 'accessorySet' ][, string $formName = 'editForm' ][, int|null $excludeId = null ]) : PickerConfig

Self-reference: $excludeId (id of the edited set) is excluded from the suggest (NotEqualFilter) and from the modal (pickerExclude), same technique as ProductCategoryPickerTrait::productCategoryParentPicker().

Parameters
$container : string = 'accessories'
$targetField : string = 'accessorySetId'
$displayField : string = 'accessorySet'
$formName : string = 'editForm'
$excludeId : int|null = null

id of the edited ProductSet (self-reference)

Return values
PickerConfig

productSetFeePicker()

Picker of a fee in the `fees` multiplier row (many-to-many `ProductSet` to `ProductSetFee`).

protected productSetFeePicker([string $container = 'fees' ][, string $targetField = 'feeId' ][, string $displayField = 'fee' ][, string $formName = 'editForm' ]) : PickerConfig
Parameters
$container : string = 'fees'
$targetField : string = 'feeId'
$displayField : string = 'fee'
$formName : string = 'editForm'
Return values
PickerConfig

productSetItemPicker()

Picker of a set composition item: `setItems` row targeting another set (a ready bundle too, i.e. nested sets). Links to `:Admin:Eshop:ProductSet` so the item detail opens from the row.

protected productSetItemPicker([string $container = 'setItems' ][, string $targetField = 'childSetId' ][, string $displayField = 'childSet' ][, string $formName = 'editForm' ][, array<int, int> $excludeIds = [] ][, int|null $excludeId = null ]) : PickerConfig

IMPORTANT: the suggest list excludes the set itself and all its ancestors ($excludeIds, see productSetAncestorIds()), otherwise a cycle A contains B contains A could be offered. The modal grid can exclude only one id (pickerExclude), so only the set itself is excluded there; the authoritative backstop is the guard in the Api ProductSetManager, which rejects the save with a clear message.

Parameters
$container : string = 'setItems'
$targetField : string = 'childSetId'
$displayField : string = 'childSet'
$formName : string = 'editForm'
$excludeIds : array<int, int> = []

the set itself plus its ancestors (suggest)

$excludeId : int|null = null

id of the edited set (modal)

Return values
PickerConfig

productSetLinkPicker()

Picker of a link in the `links` multiplier row (many-to-many `ProductSet` to `ProductSetLink`).

protected productSetLinkPicker([string $container = 'links' ][, string $targetField = 'linkId' ][, string $displayField = 'link' ][, string $formName = 'editForm' ]) : PickerConfig
Parameters
$container : string = 'links'
$targetField : string = 'linkId'
$displayField : string = 'link'
$formName : string = 'editForm'
Return values
PickerConfig

productSetVariantPicker()

Picker of the sold variant: top-level `variantId` of a set with `type = PRODUCT`. The 1:1 relation is guarded by the Api guard and a unique index.

protected productSetVariantPicker([string $formName = 'editForm' ][, string $targetField = 'variantId' ]) : PickerConfig

Outside the multiplier, so no targetContainer or displayField.

NOTE: targetPresenter is null because ProductVariant has no admin grid (it lives only as a multiplier row on the product), so the picker is suggest-only.

Parameters
$formName : string = 'editForm'
$targetField : string = 'variantId'
Return values
PickerConfig

productTagPicker()

Picker of a tag in the `tags` multiplier row (many-to-many `ProductSet` to `ProductTag`).

protected productTagPicker([string $container = 'tags' ][, string $targetField = 'tagId' ][, string $displayField = 'tag' ][, string $formName = 'editForm' ]) : PickerConfig
Parameters
$container : string = 'tags'
$targetField : string = 'tagId'
$displayField : string = 'tag'
$formName : string = 'editForm'
Return values
PickerConfig

setTemplateFile()

protected setTemplateFile(string $dir, string $filename) : void
Parameters
$dir : string
$filename : string

setTranslator()

protected setTranslator(ITranslator $translator) : self
Parameters
$translator : ITranslator
Return values
self

str()

Converts a mixed form value to a string safely.

protected str(mixed $value) : string
Parameters
$value : mixed
Return values
string

strOrNull()

Converts a mixed value to a string or null (an empty string is stored as null).

protected strOrNull(mixed $value) : string|null
Parameters
$value : mixed
Return values
string|null

addFeedOverrideModeSelect()

Builds the value mode select: one per root node (children and repetitions of a `multiple` node do not get one).

private addFeedOverrideModeSelect(Form $form, int $nodeId) : void

It sits next to the multiplier, not in its copy: the mode belongs to the whole (parameter x set) pair, so inside copies it would exist several times and repetitions could contradict each other. The template renders it as the second input-group element of a single-field node, otherwise in the card header.

NOTE: aria-label is translated manually through the translator (not by key): the field label is not rendered (it would break the [text][select] pair in an input-group, and the header already shows the tag), so without it the select would have no accessible name.

Parameters
$form : Form
$nodeId : int

addFeedOverrideMultiplier()

Builds the multiplier of one node; a copy is one repetition of the XML node.

private addFeedOverrideMultiplier(Form $form, array<string|int, mixed> $node) : void

A node without children has the field value in a copy, a node with children has child_<childId> per child (the parent value is not shown).

minCopies = 1: an empty copy is dropped on save, so one empty row creates nothing but shows where the value is entered. IMPORTANT: for a node without override rows the prefill therefore gives no key at all; an empty array would tell the multiplier "0 copies" instead of letting minCopies apply.

maxCopies: unlimited (null) for a multiple node, exactly the saved row count (normally 1) otherwise. The hard cap guarantees that all saved data is rendered and cannot be dropped silently.

Only a multiple node gets the "add" button. "Remove" is always registered but the library detaches it while there are only minCopies copies, so a non-multiple node shows it only in the anomaly (more than one saved row), where it is the only way to remove the surplus.

Parameters
$form : Form
$node : array<string|int, mixed>

addInitialStockFields()

Adds the per-warehouse initial stocking fields (quantity, price) and the shared currency.

private addInitialStockFields(Form $form) : void

Wizard only (new variant, the isWizard() block of FormTrait::createComponentEditForm()). The fields sit directly on the edit form, without a multiplier, because warehouses are a fixed set. All fields are optional: zero or empty means no receipt for that warehouse, see collectInitialStockRows().

Parameters
$form : Form

addPriceGroupCurrencyFields()

Adds the per-currency cells (price, original price, points) for one group; shared so that `group{id}` and the base `groupAll` have identical fields.

private addPriceGroupCurrencyFields(Container $groupContainer) : void
Parameters
$groupContainer : Container

addProductSetBaseFields()

Adds the basics: codes, unit, type, owning product, state, publication and goods parameters.

private addProductSetBaseFields(Form $form) : void
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'

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

buildFeedOverrideDefaults()

Builds the prefill from {@see \App\UI\Admin\Eshop\Models\Managers\FeedManager::getParameterRelsForSet()} (order `r.id ASC`, the order of repetitions in the XML).

private buildFeedOverrideDefaults() : array<string, mixed>

IMPORTANT: the multiplier is filled by a nested array in a single setDefaults(), never by a manual addCopy(). The copy count is the node rowCount, child values of copy i come from the i-th row of each child.

Return values
array<string, mixed>

buildFeedParameterRenderTree()

Converts parameter tree nodes into the shape used by the form and the rendering.

private buildFeedParameterRenderTree(array<string|int, mixed> $nodes, string $prefix, array<int, array<int, string>> $rows) : array<int, array<string, mixed>>

Node keys: id, tag (bare XML tag shown in the UI, indentation carries the hierarchy), caption (full path through the parent, PARAM > VAL, kept by the form control for error messages), multiple, actionLabel (source of the computed value), rowCount (repetitions already saved), anomaly (non-multiple parameter with more than one row) and children.

Parameters
$nodes : array<string|int, mixed>
$prefix : string
$rows : array<int, array<int, string>>
Return values
array<int, array<string, mixed>>

buildPriceSnapshotDefaults()

Builds the matrix prefill for the edited snapshot: rows of the given `publishedTime` grouped by tier (`fromAmount`), inside by group x currency (matches the multiplier containers).

private buildPriceSnapshotDefaults(ProductSet $set, string $publishedTime) : array{sameForAll: bool, tiers: list>}

The "same price for all groups" checkbox is detected in reverse: when all groups have identical price, original price and points for every tier x currency (isPriceTiersSameForAllGroups()), it is prefilled checked and groupAll takes the values of any group; otherwise it stays unchecked and only the per-group matrix is filled. So the checkbox matches the data after reopening.

Parameters
$set : ProductSet
$publishedTime : string
Return values
array{sameForAll: bool, tiers: list>}

buildPriceSnapshotModalContent()

Renders the modal content (partial) to a string for `ModalWindow::setContent()`.

private buildPriceSnapshotModalContent() : string
Return values
string

buildPriceTierMatrix()

Adds the tier x group x currency matrix to the form: the `$form['tiers']` multiplier with create and remove buttons.

private buildPriceTierMatrix(Form $form, callable(): void $onChange) : void

Shared by the edit modal (createComponentPriceSnapshotForm(), $onChange regenerates the whole modal) and by wizard step 2 on add (ProductSetEdit::Traits\FormTrait::createComponentEditForm(), $onChange redraws the wizardPriceStep snippet).

Parameters
$form : Form
$onChange : callable(): void

called after a tier is added or removed (create/remove button and multiplier onCreate/onRemove); the caller decides what to redraw

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

buildProductSetPriceHistory()

Returns the read-only price list history for the tab: snapshots grouped by `publishedTime` (newest first) with an `editable` flag.

private buildProductSetPriceHistory(ProductSet $set) : array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>

The flag is only a UI hint for icons and badges; the server enforces immutability even for a forged signal.

Snapshot rows are further grouped into groups by user group (one table per group, in the order of $this->userGroupOptions, inside by currency x tier). rowCount is the total number of rows across groups (for the summary counter).

Parameters
$set : ProductSet
Return values
array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>

buildStockReceiveModalContent()

private buildStockReceiveModalContent() : string
Return values
string

buildStockTransferModalContent()

private buildStockTransferModalContent() : string
Return values
string

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

collectFeedOverrideDefaults()

Fills defaults of one node. Children are not reached by recursion: they live in the parent copy.

private collectFeedOverrideDefaults(array<string|int, mixed> $node, array<int, array<int, string>> $byParameter, array<string, mixed> &$defaults) : void
Parameters
$node : array<string|int, mixed>
$byParameter : array<int, array<int, string>>
$defaults : array<string, mixed>

collectFeedOverrideNodeRows()

Collects the rows of one node (see the trait docblock: why a node with children also gets a parent row and why an empty child is stored as an empty string).

private collectFeedOverrideNodeRows(array<string|int, mixed> $node, array<string, mixed> $values, array<int, array{feedParameterId: int, value: string, valueMode: string}> &$rows) : void

IMPORTANT: a node in a mode other than change that produced no repetition (the admin filled nothing) gets one carrier row with an empty value; otherwise there is nowhere to store the mode and none would be lost silently. In mode change an empty field still creates no row, otherwise sets would fill with empty rows and rowCount would lie. The generator skips a carrier row in prepend/append as an empty repetition (FeedXmlGenerator::hasManualValue()) and renders no node in none.

Children always get change: the mode belongs to the parent and the generator ignores it for children.

Parameters
$node : array<string|int, mixed>
$values : array<string, mixed>
$rows : array<int, array{feedParameterId: int, value: string, valueMode: string}>

collectFeedOverrideRows()

Converts form values into flat bridge rows, in the order the repetitions must appear in the XML (the bridge stores them sequentially and the generator reads them `ORDER BY r.id`).

private collectFeedOverrideRows(array<string, mixed> $values) : array<int, array{feedParameterId: int, value: string, valueMode: string}>
Parameters
$values : array<string, mixed>
Return values
array<int, array{feedParameterId: int, value: string, valueMode: string}>

collectInitialStockRows()

Collects the initial stocking rows to be sent as receipts.

private collectInitialStockRows(ArrayHash<string|int, mixed> $values) : array<int, array{quantity: float, unitPrice: float|null}>

Only warehouses with a quantity above zero are included; zero or empty means the admin does not stock that warehouse and no receipt is created.

Parameters
$values : ArrayHash<string|int, mixed>
Return values
array<int, array{quantity: float, unitPrice: float|null}> —

row per warehouse id

collectPriceSnapshotRows()

Collects the matrix rows to send to the bridge; an empty price means the combination is not stored. The Api manager decides the rest (collisions, guard, denormalized recalculation).

private collectPriceSnapshotRows(ArrayHash<string|int, mixed> $values) : array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>
Parameters
$values : ArrayHash<string|int, mixed>
Return values
array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>

createMultiplierComponent()

Creates a multiplier for the component.

private createMultiplierComponent(string $name) : Multiplier<string|int, IComponent>
Parameters
$name : string
Return values
Multiplier<string|int, IComponent>

createNewComponentInstance()

Creates a new component instance without adding it to the presenter; used by the multiplier.

private createNewComponentInstance(string $baseName[, int|string|null $uniqueKey = null ]) : IComponent|null
Parameters
$baseName : string
$uniqueKey : int|string|null = null
Return values
IComponent|null

Creates a link from the typed URL and returns it to the picker for selection.

private createProductSetLink(string $text) : array{id: int, label: string}|null

IMPORTANT: an exact, case-insensitive match of the address is looked up first (via the existing suggest) and returned as is, so the picker cannot generate duplicates in the code list. The JS check is only a courtesy; this one is binding.

The address is written to all languages because ProductSetLinkText::$value is required and a link without an address would be unusable in the other languages. The name stays empty and can be filled in on the target presenter.

Parameters
$text : string
Return values
array{id: int, label: string}|null

createStandardComponent()

Creates a standard component (not a multiplier).

private createStandardComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

currentStockItemId()

Stock item id of the edited variant; null when it has no card yet ({@see ensureStockItemCard()} creates it at the first stock action).

private currentStockItemId() : int|null
Return values
int|null

currentUserId()

Returns the logged-in admin id, stored as `userId` of the stock document.

private currentUserId() : int|null
Return values
int|null

ensureStockItemCard()

Creates the stock item card of a legacy variant at its first stock action (receipt or transfer); no-op when the card already exists.

private ensureStockItemCard() : void

The admin has no picker or checkbox to create the card by hand. $this->variantId is always a saved variant here, because the sub-modals open only from editing; in the wizard the card is created only by FormTrait::saveProductVariantForm(). The edited set is reloaded before currentStockItemId() is read again, since the constructor-time editedSet is stale. Best effort: on failure currentStockItemId() stays null and the caller shows the noStockItem message.

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

extractPriceCell()

Extracts one cell (price, original price, points) from the raw tier array.

private extractPriceCell(array<string, mixed> $tier, int|string $groupId, string $currencyKey) : array<string, mixed>|null

The is_array() guards confirm to PHPStan that the nested group and currency values are arrays. Returns a newly built array with literal keys (not $group[$currencyKey] directly), which gives the exact shape instead of a generic array<mixed, mixed> that would fail the declared return type.

Parameters
$tier : array<string, mixed>
$groupId : int|string
$currencyKey : string
Return values
array<string, mixed>|null

extractPriceGroupBase()

Returns the cells of one representative group of a tier as the base for `groupAll`; empty when no group exists (empty tier).

private extractPriceGroupBase(array<string, mixed> $tier) : array<string, mixed>

Called only after isPriceTiersSameForAllGroups() confirmed identical groups, so the chosen group does not matter. Cells are rebuilt via extractPriceCell(), which returns literal keys, so PHPStan can verify the array<string, mixed> shape.

Parameters
$tier : array<string, mixed>
Return values
array<string, mixed>

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

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>

groupPriceRowsByUserGroup()

Turns the rows of one snapshot into one table per user group (order of `$this->userGroupOptions`, unknown groups last), sorted by currency x tier inside. The group is the table heading, so rows do not carry it.

private groupPriceRowsByUserGroup(array<int, array<string, mixed>> $rows) : array<int, array{userGroup: string, rows: list>}>
Parameters
$rows : array<int, array<string, mixed>>
Return values
array<int, array{userGroup: string, rows: list>}>

httpRequest()

Returns the HTTP request through the presenter; a control has no `getHttpRequest()`.

private httpRequest() : IRequest
Return values
IRequest

isPriceTiersSameForAllGroups()

Returns true when every tier x currency has identical values across all user groups (a missing row is null and must match too).

private isPriceTiersSameForAllGroups(array<int|string, array<string, mixed>> $tiers) : bool

An empty matrix or no groups is trivially true, so a new snapshot keeps the default "For all".

$tiers is keyed by getFromAmount() (?int, so PHPStan also sees the implicit string conversion of a null key); values are the raw arrays of buildPriceSnapshotDefaults().

Parameters
$tiers : array<int|string, array<string, mixed>>
Return values
bool

isSignalRequest()

Tells whether a signal is being handled; a control has no `getSignal()`, only the presenter has.

private isSignalRequest() : bool
Return values
bool

isSnapshotEditable()

Tells whether a snapshot may be edited or deleted: only one dated today or scheduled in the future. `publishedTime` is a lexicographically comparable string.

private isSnapshotEditable(string $publishedTime) : bool

IMPORTANT: publishedTime is UTC, so the "today" boundary is Prague calendar midnight converted to UTC, not raw date('Y-m-d'); otherwise the admin would refuse to edit even today's snapshot shortly after midnight (same approach as ProductSetManager::todayMidnight()).

Parameters
$publishedTime : string
Return values
bool

isWizard()

Tells whether a new variant is being added: the `product-variant` mode with a new (unsaved) variant, created from scratch or as a duplicate (`$duplicateFromSetId`); in both cases `$variantId` is 0 or null.

private isWizard() : bool

When true, Templates/Default/Default.latte renders a multi-step wizard instead of the tabs. Editing (variantId above 0) and the standalone and bundle modes always get false.

Return values
bool

loadAvailability()

Loads the stock panel data. `available` is never cached: it is a live query on every render, even within one component instance.

private loadAvailability() : array{availableSets: ?int, inStock: bool, deliveryDays: ?int, infoText: ?string, attributeClass: ?string, unmappedVariantIds: list, unresolvedSetIds: list}|null
Return values
array{availableSets: ?int, inStock: bool, deliveryDays: ?int, infoText: ?string, attributeClass: ?string, unmappedVariantIds: list, unresolvedSetIds: list}|null

loadContextProduct()

Loads the edited product (outside the set) in the `product-variant` mode, lazily; used for the GET prefill of new variant texts ({@see Traits\FormTrait::setProductSetFormDefaults()}) and for guards. `$productId` is server-side context from `ProductPresenter::createComponentSetEdit()`, never from the form.

private loadContextProduct() : Product|null
Return values
Product|null

loadCurrentPricePanel()

Loads the current price panel (price list tab) through `PriceResolver` (bridge action), not through the denormalized column.

private loadCurrentPricePanel() : array<int, array{price: string, originalPrice: string, points: int, fromAmount: int, currencyId: int}>
Return values
array<int, array{price: string, originalPrice: string, points: int, fromAmount: int, currencyId: int}>

loadDuplicateSourceSet()

Loads the source selling set of a duplication, lazily; only for a new (unsaved) variant in the `product-variant` mode with the constructor context `$duplicateFromSetId` (supplied server-side by `ProductPresenter::createComponentSetEdit()`, which also ran the variant ownership guard). The GET defaults ({@see Traits\FormTrait::setProductSetFormDefaults()}, {@see Traits\ParameterFieldsTrait}) take scalars, texts, relations and parameters from it instead of the plain product name prefill. The `code`, the gallery (`assetRels`) and the stock item mapping (`stockItemId`) are not read from it, so they are not copied: a duplicate needs a new code and has no images.

private loadDuplicateSourceSet() : ProductSet|null

Loaded with EDIT_INCLUDES.

Return values
ProductSet|null

loadFeedOverrideModes()

Returns the value modes of the edited set, `feedParameterId => mode`. Filled together with the rows ({@see loadFeedOverrideRows()}), it is the same bridge query.

private loadFeedOverrideModes() : array<int, string>
Return values
array<int, string>

loadFeedOverrideRows()

Returns the override rows of the edited set across all feeds, `feedParameterId => list<value>`.

private loadFeedOverrideRows() : array<int, array<int, string>>
Return values
array<int, array<int, string>>

loadFeedOverrideStructure()

Returns the feeds with their parameter trees, using one bridge query per feed (memoized).

private loadFeedOverrideStructure() : array<int, array{feedId: int, feedName: string, tree: list>}>

Form building, value collection on save and render() all read it. It is the only axis: the form is not "one field per parameter" but a multiplier whose limits depend on multiple and on the number of saved rows, so a separate flat parameter list would drift from the tree.

Return values
array<int, array{feedId: int, feedName: string, tree: list>}>

loadFeedParameterActionLabels()

Returns the catalog of parameter actions, `actionId => translated name` (read-only).

private loadFeedParameterActionLabels() : array<int, string>

Used only for the "value source" caption in the parameter card header; no action is evaluated here because that would require running the generator over the set.

Return values
array<int, string>

loadProductSetSelectData()

Loads the form axes (currencies, groups, regions, VAT, states) once per request, before the fields are built.

private loadProductSetSelectData() : void

loadStockLevels()

Returns the read-only rows of the per-warehouse stock table.

private loadStockLevels(int $stockItemId) : array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>

Never cache: stock levels must come from a live query, same as loadAvailability().

Parameters
$stockItemId : int
Return values
array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>

loadWarehouseOptions()

Returns the active warehouses for a select (id => "code — name"), only `activated=true`.

private loadWarehouseOptions() : array<int, NotTranslate>

The name is already translated DB data, not a key, hence NotTranslate; otherwise the form domain translator looks it up as a key and Tracy reports a missing translation. warehouseLabel() returns a plain string because it is also used outside selects.

Return values
array<int, NotTranslate>

parsePickerName()

Splits a picker instance name into the config key and the multiplier row indices; see {@see \App\UI\Admin\System\Presenters\Traits\Pickers\PickerHostTrait::parsePickerName()} for the full explanation of the separator.

private parsePickerName(string $name) : array{0: string, 1: list}
Parameters
$name : string
Return values
array{0: string, 1: list}

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

productLabel()

Returns the product name in the current language (first non-empty as fallback).

private productLabel(Product $product) : string
Parameters
$product : Product
Return values
string

productSetAncestorIds()

Returns the ancestors of a set in the composition tree, i.e. the sets that contain it directly or indirectly.

private productSetAncestorIds(int $setId[, int $maxDepth = 10 ]) : array<int, int>

Walks level by level (one query per level) with a depth cap and a visited set, so the picker cannot loop even if a cycle gets into the data.

The Api has the same logic in SetCompositionService (authoritative guard on save); this is only UI prevention, because the admin must not use Api services.

Parameters
$setId : int
$maxDepth : int = 10
Return values
array<int, int>

productSetFeeLabel()

Returns the fee name in the current language (first non-empty as fallback).

private productSetFeeLabel(ProductSetFee $fee) : string
Parameters
$fee : ProductSetFee
Return values
string

productSetItemExcludeIds()

Returns the sets the composition picker must not offer: the edited set itself and all its ancestors (a set cannot contain itself). The authoritative guard is in the Api manager; this is UI prevention.

private productSetItemExcludeIds() : array<int, int>

IMPORTANT: memoized per component instance. pickers() is called for every picker instance (a multiplier has one component per row), so without the cache the ancestor lookup (one bridge query per tree level) would run ten times per form render.

Return values
array<int, int>

productSetLabel()

Returns `<code> — <name>` in the current language (falls back to whichever part is non-empty).

private productSetLabel(ProductSet $set) : string
Parameters
$set : ProductSet
Return values
string

productSetLinkLabel()

Returns the link label: its URL in the current language (first non-empty address as fallback).

private productSetLinkLabel(ProductSetLink $link) : string

The name is deliberately not used: it is often shared by several links or missing, which would make links indistinguishable in the picker and leave the text field empty.

Parameters
$link : ProductSetLink
Return values
string

productTagLabel()

Returns the tag name in the current language (first non-empty as fallback).

private productTagLabel(ProductTag $tag) : string
Parameters
$tag : ProductTag
Return values
string

productVariantLabelsOf()

Returns the labels of all variants of a product: `variantId => "<product name> (#<variant id>)"`.

private productVariantLabelsOf(Product $product) : array<int, string>
Parameters
$product : Product
Return values
array<int, 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

requireStockMovePrivilege()

Enforces the stock privileges, one set for opening the sub-modal and for its submit.

private requireStockMovePrivilege() : void

IMPORTANT: the submit of stockReceiveForm / stockTransferForm is a separate routable POST; without the check there a role with Product:edit but no Store privileges could trigger a receipt or transfer by a hand-built POST. Call it as the first statement, before ensureStockItemCard() and before calling the Api. The visibility counterpart is canMoveStock() (same pair of keys).

resolveEditedSet()

Returns `preloadedSet` (the presenter already has it from `actionEdit()`), otherwise loads the set by `setId`.

private resolveEditedSet() : ProductSet|null

In the product-variant mode the modal gets only variantId (multiplier key) and finds the selling set (type 1) of that variant itself; variantId 0 (new variant) means no set.

Return values
ProductSet|null

resolvePriceVatPercents()

Returns the VAT rate for the client-side "price with VAT" computing when the form has no VAT select (the "Price list" edit modal); null when the rate is unknown (no seller or supply kind), then no preview is shown.

private resolvePriceVatPercents() : string|null

The rate comes from the unified engine (TaxManager by the supply kind of the set). The admin must not call the engine directly, so it uses the same bridge as the "current price" panel (ProductSetPresenter::internalActionCurrentPrices(), keys vat/withVat).

This is only an input aid: nothing with VAT is stored, so a regional difference does not affect saved data.

Return values
string|null

resolveProductLabel()

private resolveProductLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveProductSetAccessoryLabel()

private resolveProductSetAccessoryLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveProductSetFeeLabel()

private resolveProductSetFeeLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveProductSetLinkLabel()

private resolveProductSetLinkLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveProductTagLabel()

private resolveProductTagLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveProductVariantLabel()

private resolveProductVariantLabel(int $id) : string|null
Parameters
$id : int
Return values
string|null

resolveSubmittedSnapshotTime()

Returns the validity time from the form for the Api (`resolvePublishedTime()` takes the string as Europe/Prague).

private resolveSubmittedSnapshotTime(mixed $submitted, string|null $replaceTime) : string|null

When the edit did not change the time, the original UTC identity is sent with an explicit offset; otherwise in the ambiguous hour at the end of daylight saving time (02:00-02:59 occurs twice) the snapshot would shift by an hour and collide with its neighbor.

Parameters
$submitted : mixed
$replaceTime : 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

setPriceSnapshotFormDefaults()

Prefills the form when editing; a new snapshot (`editingPriceSnapshotTime === null`) stays empty.

private setPriceSnapshotFormDefaults(Form $form) : void
Parameters
$form : Form

setProductSetFormDefaults()

private setProductSetFormDefaults(Form $form) : void
Parameters
$form : Form

showPriceSnapshotModal()

Fills the shared admin `ModalWindow` with the current form state and opens it.

private showPriceSnapshotModal() : void

snapshotErrorKey()

Maps a bridge response code (`OK`, `IMMUTABLE`, `TIME_COLLISION`, `NOT_FOUND`, `DENORM`, `VALIDATION`) to a message key.

private snapshotErrorKey(string $domain, string $code) : string
Parameters
$domain : string
$code : string
Return values
string

suggestProducts()

Suggests products by a `like` match on the product name.

private suggestProducts(string $query) : array<int, array{id: int, label: string}>
Parameters
$query : string
Return values
array<int, array{id: int, label: string}>

suggestProductSetAccessories()

Suggests sets by a `like` match on the set name, optionally without the edited set (a set cannot be its own accessory).

private suggestProductSetAccessories(string $query[, int|null $excludeId = null ]) : array<int, array{id: int, label: string}>
Parameters
$query : string
$excludeId : int|null = null
Return values
array<int, array{id: int, label: string}>

suggestProductSetFees()

Suggests fees by a `like` match on the fee name.

private suggestProductSetFees(string $query) : array<int, array{id: int, label: string}>
Parameters
$query : string
Return values
array<int, array{id: int, label: string}>

suggestProductSetItems()

Suggests composition items by a `like` match on the set name, excluding `$excludeIds`.

private suggestProductSetItems(string $query[, array<int, int> $excludeIds = [] ]) : array<int, array{id: int, label: string}>
Parameters
$query : string
$excludeIds : array<int, int> = []
Return values
array<int, array{id: int, label: string}>

Suggests links by a `like` match on the URL (`translations.value`) only, not on the name: names are often identical or missing, so only the address tells links apart.

private suggestProductSetLinks(string $query) : array<int, array{id: int, label: string}>
Parameters
$query : string
Return values
array<int, array{id: int, label: string}>

suggestProductTags()

Suggests tags by a `like` match on the tag name.

private suggestProductTags(string $query) : array<int, array{id: int, label: string}>
Parameters
$query : string
Return values
array<int, array{id: int, label: string}>

suggestProductVariants()

Suggests variants by a `like` match on the owning product name (a variant has no text of its own).

private suggestProductVariants(string $query) : array<int, array{id: int, label: string}>

IMPORTANT: query from the product side (productManager with includes: ['translations','variants']), not from the variant side via includes: ['product.translations']. ProductVariant::$product is a back-reference (inversedBy: 'variants') that OrmEntityHydrator always drops on reads, so getProduct() would be null and the label would fall back to a bare #7.

Parameters
$query : string
Return values
array<int, array{id: int, label: string}>

toDateTimeLocalValue()

Converts `Y-m-d H:i:s` (ORM string, UTC) to a `datetime-local` value in Prague time.

private toDateTimeLocalValue(string $publishedTime) : string

Saving (Api ProductSetManager::resolvePublishedTime()) takes the input as Europe/Prague and converts to UTC; a prefill without the reverse conversion would shift the snapshot back by 1-2 h on every save.

Parameters
$publishedTime : string
Return values
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

warehouseLabel()

Returns `code — name` (name in the current language, falling back to the first non-empty one).

private warehouseLabel(Warehouse $warehouse) : string
Parameters
$warehouse : Warehouse
Return values
string
On this page

Search results