CoolCMS API

ImportProductWriter
in package

FinalYes

Writes one `<product>` card with its variants into the catalog.

Writes only through the existing Api managers (hooks, validation and search-index suppression stay in effect), never raw SQL. The writer does not judge rules: it applies a ready ImportProductVerdict. The validator already logged soft errors, so an unknown category or unit is skipped silently and only importIncomplete is set; the writer logs just what fails during the write.

Create order: card (activated=0), variants, sets (activated=0), deferred sections, then activation last. A failure midway marks all items of the card as errors and runs the compensating cleanup rollbackCreated() (sets, variants, card deleted through the managers). Without it an inactive card with an orphan variant would stay and a corrected file would never activate it. If the cleanup fails too, the leftovers are logged for manual deletion. A new variant under an existing card (createItemUnderCard()) is rolled back the same way. On update a partial run is possible and is logged as an error.

IMPORTANT: applyParameters() and applyTags() mutate the collections of the loaded or fresh set in place, before the single ProductSetManager::save(). Never exchange the whole collection (a reused instance would delete its row), and load the set with tags/parameterRels includes on update, because the save replaces the object graph and a missing include would null the existing rows.

Accessories are a second pass: applyAccessories() only queues the request (it may reference a code created later in the file); resolveAccessories() writes it after all cards.

IMPORTANT: prices and stock (applyDeferredSections()) run after the set is saved, outside its graph. StockDocumentManager::adjust() clears the identity map after commit, so no loaded entity may be held there: the writer works with ids and raw DBAL reads. A soft error found at that point is stored with updateColumn('importIncomplete', true), not into the saved graph.

Images are deduplicated through Asset.meta.importSource (see resolveImageBySrc()); a new download goes through the base64 meta['image'] channel, so the writer never creates an Asset itself and finds the new id after the save.

A flush failure closes the EntityManager for the rest of the process and a rejected save leaves replaced collections in the UoW, so after every failure recoverEntityManager() clears the UoW and reopens the manager.

Table of Contents

Properties

$assetManager  : AssetManager
$em  : EntityManagerInterface
$imageDownloader  : ImportImageDownloader
$parameterManager  : ParameterManager
$producerManager  : ProducerManager
$productCategoryManager  : ProductCategoryManager
$productManager  : ProductManager
$productSetManager  : ProductSetManager
$productVariantManager  : ProductVariantManager
$stockDocumentManager  : StockDocumentManager
$stockItemManager  : StockItemManager
$unitOfWorkReset  : UnitOfWorkReset

Methods

__construct()  : mixed
applyImages()  : bool
Writes the set gallery through `meta['images']` and {@see ProductSetManager::processImages()} (DELETE and INSERT; `assetRels` is always a skip-field of the graph, so this channel touches nothing else on the set). Semantics:
applyPrices()  : bool
Writes a new point of the price series through {@see ProductSetManager::savePriceSnapshot()} at run time (`publishedTime = null` means "now"; the immutable guard does not allow the past).
applySetTexts()  : void
Applies the set texts per language: name (not null), longName (nullable), shortDescription and description (HTML, sanitized).
applyTags()  : bool
Applies the set tags (many-to-many, match-only on `ProductTagText.name`). The validator already logged an unknown tag as a soft error, so it is skipped silently. Mutates `$set->getTags()` in place, the same way as {@see applyCategories()} on the card. Always returns `false`: tags are not created, so no error can be found only during the write.
markSetIncomplete()  : bool
Sets `importIncomplete = 1` with a dedicated `updateColumn` on an already saved set (second pass, sections after the save). The Result is checked: a failure is logged as an error (otherwise the set would look complete) and the caller decides about the summary. An exception is not propagated (the marker is best-effort diagnostics, not a catalog write); the EM is recovered after it.
resolveAccessories()  : void
Second pass for accessories, called by {@see ImportExecutor} after all cards are processed (after `ImportValidator::finish()`, before the run ends).
write()  : void
Writes one card according to the verdict and records counts and `touchedSetIds` in `$context->summary`.
activateSet()  : void
Final set activation: `updateColumn` bypasses hooks (the search index is suppressed anyway, the reindex runs after the import).
applyAccessories()  : void
Only registers the request in the queue (second pass); the write is in {@see resolveAccessories()}.
applyCategories()  : bool
Applies the card categories: `href` / `path`, merge (default) | replace, `remove="1"`.
applyDeferredSections()  : void
Applies the variant sections that run after the set is saved and before activation (on create).
applyParameters()  : bool
Applies the set parameters with per-key granularity: a mentioned `key` replaces all of its rows (multiselect/checkboxlist = N rows), unmentioned keys stay unchanged, `remove="1"` deletes all rows of the key, and `collectionMode=replace` on `<parameters>` makes the collection exactly match the file (unmentioned keys disappear). An unknown parameter or option (without `autoCreateOptions`) was logged by the validator as a soft error and is skipped silently here.
applyProductFields()  : bool
Applies the scalar fields of the card: producer (auto-create per settings), type (match-only) and `activated` (update only; on create the activation comes last).
applyProductTexts()  : void
Applies the card texts per language. On create without `<name>` in the default language the name is taken from the first created variant (single-variant goods are usually named by the variant only).
applySetFields()  : bool
Applies the scalar fields of the set. Returns `true` for a soft error found only now (currently none - unknown code lists were logged by the validator and are just skipped here).
applyStock()  : bool
Writes the stock through the movement journal, the only write channel: {@see StockDocumentManager::adjust()} (stocktake document) brings the level to the target quantity.
assetExists()  : bool
buildAccessory()  : ProductSetAccessory
buildParameterRelRow()  : ProductSetParameterRel|null
Builds one new `ProductSetParameterRel` row by parameter type; `null` means the value could not be assigned.
buildUniqueCategoryHref()  : string
buildUniqueOptionKey()  : string
Derived `key` of a new option (`<parentKey>-<slug(name)>`), unique across `cms_mod_eshop_parameters` (length 45).
categoryReference()  : ProductCategory
Lazy FK proxy of a category without a query; `null` here would mean a mapping error, not data.
collectNamedLanguageIds()  : array<int, true>
Returns the languages in which the file gives a non-empty `<name>`; only there may a new translation row be created.
createCard()  : void
createCategoryPath()  : int|null
Creates the missing segments of a path like `Elektro / Apple` (parent first), each with only a name and `href` (webalized, unique in the default language), without SEO texts. Created segments are registered in the run code list so later cards of the file find them.
createItemUnderCard()  : bool
Creates a new variant and set under an existing card (the code is not in the DB, other card codes are).
createParameterOption()  : int|null
Creates an unknown option as a child `Parameter` row (level 2) for `autoCreateOptions=1`, only through {@see ParameterManager} (`OptionsSyncTrait`, no raw SQL for writes).
createVariantWithSet()  : array{int, int, bool}
Creates a variant and its sales set (type=1, `activated=0`) under the given card.
findAssetIdByUrl()  : int|null
Finds an asset across runs by `sha256(url)` stored in `Asset.meta.importSource.urlHash` (not the raw URL): a hex string has no `%`, `_` or backslash, so no LIKE escaping is needed, unlike searching a JSON-escaped URL directly.
findOrCreateTranslation()  : T|null
Returns the existing translation of a language, otherwise a new one from `$factory`; `$factory === null` means the row is not created (missing name) and `null` is returned.
findProductAssetId()  : int|null
Returns the current FK of the card photo (raw DBAL; fresh after a `save()` with `meta['image']`).
findSetVariantId()  : int|null
Returns the variant of the sales set (raw DBAL: the id is enough, hydration would only fill the identity map).
findSetVatId()  : int|null
Static VAT rate of the set; read only after the set is saved, so a `<vat>` from the same file already applies.
findStockLevelQuantity()  : numeric-string
Returns the physical stock of the card including reservations through {@see StockItemManager::getAvailabilityBatch()} (the only Store read API for other modules); a warehouse without a `StockLevel` row counts as 0.
findTextInDefaultLang()  : ImportValue|null
finishGallerySlots()  : bool
Matches the gallery slots with assets after the save (positional zip). Returns `true` for a soft error (unmatched upload, alt not written).
finishProductImage()  : bool
Finishes after a successful `productManager->save($entity, $plan['meta'])`, when `$productId` already exists: a dedup hit is written with `updateColumn('asset', ...)`, a fresh upload is looked up ({@see findProductAssetId()}) and tagged with `Asset.meta.importSource` for the next dedup.
hasCardData()  : bool
Whether the file has anything to update on the card (otherwise the card is neither loaded nor saved).
hasTextInDefaultLang()  : bool
loadAssetTextRows()  : array<int, array{name: ?string, description: ?string}>
loadCurrentPrices()  : array<string, array{price: string, originalPrice: string, points: int}>
Returns the currently valid price per axis from the read-only view `cms_mod_eshop_product_set_current_prices`: the same source of truth the front end and the export ({@see \App\UI\Api\Eshop\Models\Generators\ProductXmlExporter}) read, so "same price means no new snapshot" holds exactly for what the export wrote into the file.
loadExistingImageAssetIds()  : array<int, int>
Returns the existing gallery of the set ordered by `order` (`order=1` is the main image).
markAllSetsIncomplete()  : void
Marks all sales sets of the card as incomplete after a card-level soft error. The ids are read with raw DBAL (own module table, no `find()`, so no `clearDoctrine()`), the write goes through the manager.
markAssetImportSource()  : void
Tags a freshly downloaded asset with its source (the next run finds it via {@see findAssetIdByUrl()}); merges into the existing `meta` (variant dimensions).
newParameterRel()  : ProductSetParameterRel
preResolveParameterOptions()  : bool
Pre-pass for `autoCreateOptions=1`: creates all unknown parameter options before anything from `<parameters>` is written into `ProductSetParameterRel`.
recoverEntityManager()  : void
After a failed save/flush: drops the poisoned UoW and reopens the closed EM.
rejectedUploadReasons()  : array<int, string>
Returns the reasons from `saveInfo.rejectedUploads` of a successful save result ({@see \App\Core\Traits\Api\Models\Managers\SaveTrait::finishRejectedUploads()}): one entry per rejected upload (`UploadRejection::REASON_*`, unknown shape = `?`).
resolveAccessoryTarget()  : int|null
Maps a code to a set id: first sets touched by this run (including those created later in the file), then the DB.
resolveActivated()  : bool
Resolves `<activated>` from the value (default 1).
resolveCategoryId()  : int|null
resolveImageBySrc()  : array{0: int, 1: null}|array{0: null, 1: array{name: string, data: string, url: string, sha256: string}}|null
Resolves `<image src>` to an existing assetId (dedup, no download) or freshly downloaded data for upload. Dedup order: this run ({@see ImportExecutionSummary::$urlToAssetId}), then `Asset.meta.importSource` from earlier runs ({@see findAssetIdByUrl()}), then the download ({@see ImportImageDownloader}). Shared by the gallery and the card photo (same downloader).
resolveNetAmount()  : string|null
Returns the price without VAT from the `amount`/`amountWithVat` pair; `null` means neither tag is in the file (leave untouched). An empty or non-numeric tag never gets here: the validator blocks it as a hard item error, because a price of "0 by mistake" would sell for free.
resolveProducerId()  : int|null
Finds a producer by name; an unknown one is created under `autoCreateProducers` (`name` only) and registered in the run code list.
resolveProductImageMeta()  : array{meta: array, incomplete: bool, dedupAssetId: ?int, pendingUrl: ?string, pendingSha256: ?string}
Builds the meta for the nearest `productManager->save()` (create and update call the same save and just pass this along) and decides what to do after the save ({@see finishProductImage()}).
resolveStockItemCard()  : int|null
Returns the stock item card of the variant: an existing link (raw DBAL, own table), otherwise an idempotent creation via {@see ProductVariantManager::foundStockItemCard()} (card code = set code).
rollbackCreated()  : string
Compensating cleanup after a failed create: sets, variants and card are deleted through the managers (delete hooks run; search index suppressed).
rowInt()  : int
DBAL returns `mixed`: guard against a mixed cast (see `StockReader::toInt()`).
rowString()  : string
tagReference()  : ProductTag
Lazy FK proxy of a tag without a query, same pattern as {@see categoryReference()}.
tryDelete()  : bool
updateCard()  : void
updateItem()  : bool
Updates an existing set: loads it with texts, applies changes, saves.
writeAccessoryCollection()  : bool
Writes the `accessories` of one set (merge/replace) with its own `find()` and `save()` using `includes: ['accessories']`. Mutates the collection in place (`add()`/`removeElement()`).
writeImageAlts()  : bool
Writes alt texts (`<alt>` to `AssetText.name`). {@see AssetManager::updateColumn()} on `translations` is a DELETE and INSERT of all languages of the asset, so the existing rows are loaded first ({@see loadAssetTextRows()}) and only the languages `<alt>` mentions are overwritten; another language (or a `description` created outside the import) stays untouched.

Properties

Methods

__construct()

public __construct(ProductManager $productManager, ProductVariantManager $productVariantManager, ProductSetManager $productSetManager, ProducerManager $producerManager, ProductCategoryManager $productCategoryManager, ParameterManager $parameterManager, StockDocumentManager $stockDocumentManager, StockItemManager $stockItemManager, ImportImageDownloader $imageDownloader, AssetManager $assetManager, EntityManagerInterface $em, UnitOfWorkReset $unitOfWorkReset) : mixed
Parameters
$productManager : ProductManager
$productVariantManager : ProductVariantManager
$productSetManager : ProductSetManager
$producerManager : ProducerManager
$productCategoryManager : ProductCategoryManager
$parameterManager : ParameterManager
$stockDocumentManager : StockDocumentManager
$stockItemManager : StockItemManager
$imageDownloader : ImportImageDownloader
$assetManager : AssetManager
$em : EntityManagerInterface
$unitOfWorkReset : UnitOfWorkReset

applyImages()

Writes the set gallery through `meta['images']` and {@see ProductSetManager::processImages()} (DELETE and INSERT; `assetRels` is always a skip-field of the graph, so this channel touches nothing else on the set). Semantics:

public applyImages(ImportCollectionDto<string|int, ImportImageDto>|null $images, int $setId, string|null $code, string $action, ImportExecutionContext $context) : bool
  • merge (default) sits on the existing gallery (loadExistingImageAssetIds(), ordered by order) plus new or updated items; replace matches the file exactly
  • <image assetId> is an existing asset (verified by assetExists(), otherwise a soft error)
  • <image src> goes through resolveImageBySrc(): a dedup hit (this run or Asset.meta from earlier runs) returns the existing id without downloading; otherwise a download plus a base64 row for processImages() (the asset is created inside afterSave() and its id is found only after the save, by positional zip with $slots)
  • remove="1" removes only the link (the asset is not deleted, another set may share it)
  • main="1" moves its own item to order=1 (the last main in the file wins)
  • a merged gallery above ImportRules::MAX_IMAGES_PER_VARIANT is truncated (the surplus is not attached, plus a soft error); the validator checks only the number of file items, not the merge result
  • alt texts (<alt>) are written only to items the file mentions; writeImageAlts() merges them with the existing ones (another language stays untouched)

Takes ?ImportCollectionDto $images, $code and $action directly (not ImportVariantDto/ ImportItemVerdict) so it can also serve bundle sets without a variant (ImportBundleWriter).

Parameters
$images : ImportCollectionDto<string|int, ImportImageDto>|null
$setId : int
$code : string|null
$action : string
$context : ImportExecutionContext
Return values
bool

applyPrices()

Writes a new point of the price series through {@see ProductSetManager::savePriceSnapshot()} at run time (`publishedTime = null` means "now"; the immutable guard does not allow the past).

public applyPrices(array<int, ImportPriceDto> $prices, int $setId, string|null $code, string $action, ImportExecutionContext $context) : bool

Rules:

  • the axis is currency x fromAmount (default 0 = base band, 1 is its alias) x userGroupId (default = base group); axes the file does not mention are untouched, because the view computing the maximum published time works per axis and an older point of another currency or band stays valid
  • <amount> is the price without VAT 1:1; <amountWithVat> is converted with the set's current rate (ImportRules::priceWithoutVat()); the rate is read only now, so a <vat> from the same file (written in applySetFields() before the save) already applies
  • an unmentioned <originalAmount(WithVat)>/<points> is untouched: it is taken over from the currently valid point of the axis, not zeroed (otherwise a weekly price update would delete the promotional original price)
  • an equal value on the axis means the row does not go into the snapshot; when no axis differs, savePriceSnapshot() is not called at all, so an export-import roundtrip does not extend the series

The validator caught hard errors (both tags of a pair, non-numeric or negative amount, bad fromAmount) and logged an unknown currency as soft; the writer just skips it.

Takes list<ImportPriceDto> $prices, $code and $action directly (not ImportVariantDto/ ImportItemVerdict) so it can also serve bundle sets without a variant (ImportBundleWriter).

Parameters
$prices : array<int, ImportPriceDto>
$setId : int
$code : string|null
$action : string
$context : ImportExecutionContext
Return values
bool

applySetTexts()

Applies the set texts per language: name (not null), longName (nullable), shortDescription and description (HTML, sanitized).

public applySetTexts(ProductSet $set, array<string, array<int, ImportValue>> $texts, ImportExecutionContext $context) : void

IMPORTANT: a language row that does not exist yet is created only with a non-empty <name lang>, because an empty name would override the language fallback on the front end.

Takes $texts directly (not ImportVariantDto) so it can also serve bundle sets without a variant (ImportBundleWriter).

Parameters
$set : ProductSet
$texts : array<string, array<int, ImportValue>>
$context : ImportExecutionContext

applyTags()

Applies the set tags (many-to-many, match-only on `ProductTagText.name`). The validator already logged an unknown tag as a soft error, so it is skipped silently. Mutates `$set->getTags()` in place, the same way as {@see applyCategories()} on the card. Always returns `false`: tags are not created, so no error can be found only during the write.

public applyTags(ProductSet $set, ImportCollectionDto<string|int, ImportTagDto>|null $tags, ImportExecutionContext $context) : bool

Takes ?ImportCollectionDto $tags (not ImportVariantDto) so it can also serve bundle sets without a variant (ImportBundleWriter).

Parameters
$set : ProductSet
$tags : ImportCollectionDto<string|int, ImportTagDto>|null
$context : ImportExecutionContext
Return values
bool

markSetIncomplete()

Sets `importIncomplete = 1` with a dedicated `updateColumn` on an already saved set (second pass, sections after the save). The Result is checked: a failure is logged as an error (otherwise the set would look complete) and the caller decides about the summary. An exception is not propagated (the marker is best-effort diagnostics, not a catalog write); the EM is recovered after it.

public markSetIncomplete(int $setId, int $line, string|null $code, string|null $action, ImportExecutionContext $context) : bool
Parameters
$setId : int
$line : int
$code : string|null
$action : string|null
$context : ImportExecutionContext
Return values
bool

resolveAccessories()

Second pass for accessories, called by {@see ImportExecutor} after all cards are processed (after `ImportValidator::finish()`, before the run ends).

public resolveAccessories(ImportExecutionContext $context) : void

A code is resolved first against sets created or updated in this run (ImportExecutionSummary::$codeToSetId), then against the DB (existing sets outside the file). An unknown code is a soft error (log plus importIncomplete on the source set); a set cannot be its own accessory.

Parameters
$context : ImportExecutionContext

activateSet()

Final set activation: `updateColumn` bypasses hooks (the search index is suppressed anyway, the reindex runs after the import).

private activateSet(int $setId, bool $activated) : void
Parameters
$setId : int
$activated : bool

applyCategories()

Applies the card categories: `href` / `path`, merge (default) | replace, `remove="1"`.

private applyCategories(Product $entity, ImportProductDto $product, bool $isInsert, ImportExecutionContext $context) : bool

An unknown category is skipped (validator already warned) or, under autoCreateCategories, created by path. Returns true for a soft error found only now (auto-create failed).

Parameters
$entity : Product
$product : ImportProductDto
$isInsert : bool
$context : ImportExecutionContext
Return values
bool

applyDeferredSections()

Applies the variant sections that run after the set is saved and before activation (on create).

private applyDeferredSections(ImportVariantDto $variant, int $setId, ImportItemVerdict $item, ImportExecutionContext $context) : void

Accessories may reference a code created later in the same file, so they are only registered in $context->summary->accessoryQueue; resolveAccessories() does the actual write after all cards are done. Prices, stock and images are written immediately (they only need the set or variant id).

Parameters
$variant : ImportVariantDto
$setId : int
$item : ImportItemVerdict
$context : ImportExecutionContext

applyParameters()

Applies the set parameters with per-key granularity: a mentioned `key` replaces all of its rows (multiselect/checkboxlist = N rows), unmentioned keys stay unchanged, `remove="1"` deletes all rows of the key, and `collectionMode=replace` on `<parameters>` makes the collection exactly match the file (unmentioned keys disappear). An unknown parameter or option (without `autoCreateOptions`) was logged by the validator as a soft error and is skipped silently here.

private applyParameters(ProductSet $set, ImportVariantDto $variant, ImportExecutionContext $context) : bool

Mutates $set->getParameterRels() in place (add()/removeElement()). IMPORTANT: never exchange the whole collection or reuse an existing instance in a new one - the reused instance would delete its row. Returns true when autoCreateOptions=1 and creating an option failed during the write (an error the validator could not know).

Parameters
$set : ProductSet
$variant : ImportVariantDto
$context : ImportExecutionContext
Return values
bool

applyProductTexts()

Applies the card texts per language. On create without `<name>` in the default language the name is taken from the first created variant (single-variant goods are usually named by the variant only).

private applyProductTexts(Product $entity, ImportProductDto $product, ImportVariantDto|null $nameFallback, ImportExecutionContext $context) : void

A new translation row for a language is created only with a non-empty name (see applySetTexts()).

Parameters
$entity : Product
$product : ImportProductDto
$nameFallback : ImportVariantDto|null
$context : ImportExecutionContext

applyStock()

Writes the stock through the movement journal, the only write channel: {@see StockDocumentManager::adjust()} (stocktake document) brings the level to the target quantity.

private applyStock(ImportVariantDto $variant, int $setId, ImportItemVerdict $item, ImportExecutionContext $context) : bool

IMPORTANT: StockLevel is never written directly (it is only a denormalization).

  • the warehouse is Warehouse.code; without the attribute the default is the only active one; the validator logs an unknown or ambiguous warehouse as a soft error and the section is skipped
  • the stock item card is created idempotently via ProductVariantManager::foundStockItemCard() (card code = set code, link meta['stockItemId'] to the variant)
  • mode="set" is the target physical level including reservations (StockLevel.quantity), so the movement is the difference; the same number again is a zero difference and no movement (idempotent)
  • mode="add" moves by plus or minus the value
  • a negative resulting level is a hard item error: nothing is written to stock (adjust() is not even called) and the set gets importIncomplete. A dry run cannot detect it (depends on live state)
Parameters
$variant : ImportVariantDto
$setId : int
$item : ImportItemVerdict
$context : ImportExecutionContext
Return values
bool

assetExists()

private assetExists(int $assetId) : bool
Parameters
$assetId : int
Return values
bool

buildParameterRelRow()

Builds one new `ProductSetParameterRel` row by parameter type; `null` means the value could not be assigned.

private buildParameterRelRow(ProductSet $set, int $parameterId, array{id: int, type: int|null, hasOptions: bool} $meta, ImportParameterDto $dto, ImportExecutionContext $context) : ProductSetParameterRel|null
Parameters
$set : ProductSet
$parameterId : int
$meta : array{id: int, type: int|null, hasOptions: bool}
$dto : ImportParameterDto
$context : ImportExecutionContext
Return values
ProductSetParameterRel|null

buildUniqueOptionKey()

Derived `key` of a new option (`<parentKey>-<slug(name)>`), unique across `cms_mod_eshop_parameters` (length 45).

private buildUniqueOptionKey(string $parentKey, string $name) : string
Parameters
$parentKey : string
$name : string
Return values
string

collectNamedLanguageIds()

Returns the languages in which the file gives a non-empty `<name>`; only there may a new translation row be created.

private collectNamedLanguageIds(array<string, array<int, ImportValue>> $texts, ImportCodebooks $codebooks) : array<int, true>
Parameters
$texts : array<string, array<int, ImportValue>>
$codebooks : ImportCodebooks
Return values
array<int, true>

createCategoryPath()

Creates the missing segments of a path like `Elektro / Apple` (parent first), each with only a name and `href` (webalized, unique in the default language), without SEO texts. Created segments are registered in the run code list so later cards of the file find them.

private createCategoryPath(string $path, int $line, ImportExecutionContext $context) : int|null
Parameters
$path : string
$line : int
$context : ImportExecutionContext
Return values
int|null

createParameterOption()

Creates an unknown option as a child `Parameter` row (level 2) for `autoCreateOptions=1`, only through {@see ParameterManager} (`OptionsSyncTrait`, no raw SQL for writes).

private createParameterOption(int $parentId, string $name, ImportExecutionContext $context[, string|null &$keyError = null ]) : int|null

IMPORTANT: the sync is a full replace of all options of the parent, so the meta must also carry the existing options unchanged, otherwise creating one option would delete all the others. The new key is derived (<parentKey>-<webalize(name)>, unique) and written by a separate updateColumn(), because the options sync ignores key. Returns null when creation fails; when only the key write fails (Result or exception) it returns the id and puts the reason into $keyError (the option exists).

Parameters
$parentId : int
$name : string
$context : ImportExecutionContext
$keyError : string|null = null
Return values
int|null

createVariantWithSet()

Creates a variant and its sales set (type=1, `activated=0`) under the given card.

private createVariantWithSet(int $productId, ImportVariantDto $variant, ImportItemVerdict $item, bool $cardIncomplete, ImportExecutionContext $context, array{variantIds: list, setIds: list} &$created) : array{int, int, bool}

Returns [variantId, setId, activateLast]. On failure it throws and the caller decides; the ids already created are in $created, so the caller can roll them back (rollbackCreated()).

Parameters
$productId : int
$variant : ImportVariantDto
$item : ImportItemVerdict
$cardIncomplete : bool
$context : ImportExecutionContext
$created : array{variantIds: list, setIds: list}
Return values
array{int, int, bool}

findAssetIdByUrl()

Finds an asset across runs by `sha256(url)` stored in `Asset.meta.importSource.urlHash` (not the raw URL): a hex string has no `%`, `_` or backslash, so no LIKE escaping is needed, unlike searching a JSON-escaped URL directly.

private findAssetIdByUrl(string|null $url) : int|null
Parameters
$url : string|null
Return values
int|null

findOrCreateTranslation()

Returns the existing translation of a language, otherwise a new one from `$factory`; `$factory === null` means the row is not created (missing name) and `null` is returned.

private findOrCreateTranslation(Collection<int, T> $translations, int $languageId, callable(): T|null $factory) : T|null
Parameters
$translations : Collection<int, T>
$languageId : int
$factory : callable(): T|null
Tags
template
Return values
T|null

findProductAssetId()

Returns the current FK of the card photo (raw DBAL; fresh after a `save()` with `meta['image']`).

private findProductAssetId(int $productId) : int|null
Parameters
$productId : int
Return values
int|null

findSetVariantId()

Returns the variant of the sales set (raw DBAL: the id is enough, hydration would only fill the identity map).

private findSetVariantId(int $setId) : int|null
Parameters
$setId : int
Return values
int|null

findSetVatId()

Static VAT rate of the set; read only after the set is saved, so a `<vat>` from the same file already applies.

private findSetVatId(int $setId) : int|null
Parameters
$setId : int
Return values
int|null

findStockLevelQuantity()

Returns the physical stock of the card including reservations through {@see StockItemManager::getAvailabilityBatch()} (the only Store read API for other modules); a warehouse without a `StockLevel` row counts as 0.

private findStockLevelQuantity(int $stockItemId, int $warehouseId) : numeric-string
Parameters
$stockItemId : int
$warehouseId : int
Return values
numeric-string

finishGallerySlots()

Matches the gallery slots with assets after the save (positional zip). Returns `true` for a soft error (unmatched upload, alt not written).

private finishGallerySlots(array<int, ImportImageDto}> $slots, array<int, int> $finalIds, Result $result, int $line, string|null $code, string $action, ImportExecutionContext $context) : bool
Parameters
$slots : array<int, ImportImageDto}>
$finalIds : array<int, int>

gallery of the set after the save, by order

$result : Result
$line : int
$code : string|null
$action : string
$context : ImportExecutionContext
Return values
bool

finishProductImage()

Finishes after a successful `productManager->save($entity, $plan['meta'])`, when `$productId` already exists: a dedup hit is written with `updateColumn('asset', ...)`, a fresh upload is looked up ({@see findProductAssetId()}) and tagged with `Asset.meta.importSource` for the next dedup.

private finishProductImage(int $productId, array{meta: array, incomplete: bool, dedupAssetId: ?int, pendingUrl: ?string, pendingSha256: ?string} $plan, ImportExecutionContext $context[, Result|null $saveResult = null ][, int|null $previousAssetId = null ]) : bool

IMPORTANT: $saveResult is the successful result of the same save. A rejected upload (saveInfo.rejectedUploads, UploadRejection) does not fail the save, it only leaves the FK unchanged. Then the current (old) asset must not be tagged with importSource of the new URL nor registered in the run cache, otherwise the next product with the same URL would get a foreign old photo. The same applies when the FK stayed $previousAssetId (or empty) with no reported rejection. Both are soft errors (warning plus incomplete).

Parameters
$productId : int
$plan : array{meta: array, incomplete: bool, dedupAssetId: ?int, pendingUrl: ?string, pendingSha256: ?string}
$context : ImportExecutionContext
$saveResult : Result|null = null
$previousAssetId : int|null = null
Return values
bool

loadAssetTextRows()

private loadAssetTextRows(int $assetId) : array<int, array{name: ?string, description: ?string}>
Parameters
$assetId : int
Return values
array<int, array{name: ?string, description: ?string}> —

language id to the existing AssetText

loadCurrentPrices()

Returns the currently valid price per axis from the read-only view `cms_mod_eshop_product_set_current_prices`: the same source of truth the front end and the export ({@see \App\UI\Api\Eshop\Models\Generators\ProductXmlExporter}) read, so "same price means no new snapshot" holds exactly for what the export wrote into the file.

private loadCurrentPrices(int $setId) : array<string, array{price: string, originalPrice: string, points: int}>

Raw DBAL (own module table, no find(), so no clearDoctrine()).

Parameters
$setId : int
Return values
array<string, array{price: string, originalPrice: string, points: int}> —

fromAmount|userGroupId|currencyId

loadExistingImageAssetIds()

Returns the existing gallery of the set ordered by `order` (`order=1` is the main image).

private loadExistingImageAssetIds(int $setId) : array<int, int>
Parameters
$setId : int
Return values
array<int, int>

markAllSetsIncomplete()

Marks all sales sets of the card as incomplete after a card-level soft error. The ids are read with raw DBAL (own module table, no `find()`, so no `clearDoctrine()`), the write goes through the manager.

private markAllSetsIncomplete(int $productId, ImportExecutionContext $context) : void
Parameters
$productId : int
$context : ImportExecutionContext

markAssetImportSource()

Tags a freshly downloaded asset with its source (the next run finds it via {@see findAssetIdByUrl()}); merges into the existing `meta` (variant dimensions).

private markAssetImportSource(int $assetId, string $url, string $sha256) : void
Parameters
$assetId : int
$url : string
$sha256 : string

preResolveParameterOptions()

Pre-pass for `autoCreateOptions=1`: creates all unknown parameter options before anything from `<parameters>` is written into `ProductSetParameterRel`.

private preResolveParameterOptions(ImportVariantDto $variant, ImportItemVerdict $item, ImportExecutionContext $context) : bool

IMPORTANT: must run before find() or creation of the set. createParameterOption() calls parameterManager->find()/findAll(), whose clearDoctrine() detaches all entities held in the identity map (including a set being mutated), so the later save() of the set would fail with "A new entity was found through the relationship ... tags" (loose/detached items in its untouched collections). buildParameterRelRow() then only reads $codebooks (no DB). Returns true when at least one creation failed (a soft error found during the write).

Parameters
$variant : ImportVariantDto
$item : ImportItemVerdict
$context : ImportExecutionContext
Return values
bool

recoverEntityManager()

After a failed save/flush: drops the poisoned UoW and reopens the closed EM.

private recoverEntityManager() : void

IMPORTANT: without resetting the persisters the next flush would replay the failed queue. The reflection used here is vendor-fragile and covered by a harness test.

rejectedUploadReasons()

Returns the reasons from `saveInfo.rejectedUploads` of a successful save result ({@see \App\Core\Traits\Api\Models\Managers\SaveTrait::finishRejectedUploads()}): one entry per rejected upload (`UploadRejection::REASON_*`, unknown shape = `?`).

private static rejectedUploadReasons(Result $result) : array<int, string>
Parameters
$result : Result
Return values
array<int, string>

resolveAccessoryTarget()

Maps a code to a set id: first sets touched by this run (including those created later in the file), then the DB.

private resolveAccessoryTarget(string $code, ImportExecutionContext $context) : int|null
Parameters
$code : string
$context : ImportExecutionContext
Return values
int|null

resolveImageBySrc()

Resolves `<image src>` to an existing assetId (dedup, no download) or freshly downloaded data for upload. Dedup order: this run ({@see ImportExecutionSummary::$urlToAssetId}), then `Asset.meta.importSource` from earlier runs ({@see findAssetIdByUrl()}), then the download ({@see ImportImageDownloader}). Shared by the gallery and the card photo (same downloader).

private resolveImageBySrc(string $url, string|null $code, string|null $action, int $line, ImportExecutionContext $context) : array{0: int, 1: null}|array{0: null, 1: array{name: string, data: string, url: string, sha256: string}}|null
Parameters
$url : string
$code : string|null
$action : string|null
$line : int
$context : ImportExecutionContext
Return values
array{0: int, 1: null}|array{0: null, 1: array{name: string, data: string, url: string, sha256: string}}|null —

null = download/validation failed (already logged as a soft error)

resolveNetAmount()

Returns the price without VAT from the `amount`/`amountWithVat` pair; `null` means neither tag is in the file (leave untouched). An empty or non-numeric tag never gets here: the validator blocks it as a hard item error, because a price of "0 by mistake" would sell for free.

private resolveNetAmount(ImportValue|null $amount, ImportValue|null $amountWithVat, string|null $vatPercents) : string|null
Parameters
$amount : ImportValue|null
$amountWithVat : ImportValue|null
$vatPercents : string|null
Return values
string|null

resolveProductImageMeta()

Builds the meta for the nearest `productManager->save()` (create and update call the same save and just pass this along) and decides what to do after the save ({@see finishProductImage()}).

private resolveProductImageMeta(ImportProductDto $product, string|null $code, string|null $action, ImportExecutionContext $context) : array{meta: array, incomplete: bool, dedupAssetId: ?int, pendingUrl: ?string, pendingSha256: ?string}

meta['image'] (base64) handles only a new upload or deletion; an existing assetId (dedup hit) must be written by a separate updateColumn('asset', ...), because that channel accepts no id.

Parameters
$product : ImportProductDto
$code : string|null
$action : string|null
$context : ImportExecutionContext
Return values
array{meta: array, incomplete: bool, dedupAssetId: ?int, pendingUrl: ?string, pendingSha256: ?string}

resolveStockItemCard()

Returns the stock item card of the variant: an existing link (raw DBAL, own table), otherwise an idempotent creation via {@see ProductVariantManager::foundStockItemCard()} (card code = set code).

private resolveStockItemCard(int $variantId, int $line, ImportItemVerdict $item, ImportExecutionContext $context) : int|null
Parameters
$variantId : int
$line : int
$item : ImportItemVerdict
$context : ImportExecutionContext
Return values
int|null

rollbackCreated()

Compensating cleanup after a failed create: sets, variants and card are deleted through the managers (delete hooks run; search index suppressed).

private rollbackCreated(int|null $productId, array<int, int> $variantIds, array<int, int> $setIds, int $line, string|null $code, ImportExecutionContext $context) : string

IMPORTANT: "nothing was active" does not always hold - when only the activation of the Nth set or of the card fails, earlier sets already have activated=1. A successful cleanup deletes them anyway, but leftovers of a failed cleanup may be active (the warning says so).

All sets of this item are removed from the run lookup registries (ImportExecutionSummary::forgetSets(): code to id, accessory queue), leftovers included, because they are not an accepted item and nothing may be matched to them from the cache. In touchedSetIds deleted sets are removed (a reindex would target a missing document) and leftovers are added (they exist in the DB and may be active, so the reindex must match the DB). Returns a note for the item error line; success logs an info "item rolled back", failure logs a warning with the leftovers.

Parameters
$productId : int|null
$variantIds : array<int, int>
$setIds : array<int, int>
$line : int
$code : string|null
$context : ImportExecutionContext
Return values
string

rowInt()

DBAL returns `mixed`: guard against a mixed cast (see `StockReader::toInt()`).

private rowInt(mixed $value) : int
Parameters
$value : mixed
Return values
int

rowString()

private rowString(mixed $value) : string
Parameters
$value : mixed
Return values
string

tagReference()

Lazy FK proxy of a tag without a query, same pattern as {@see categoryReference()}.

private tagReference(int $id) : ProductTag
Parameters
$id : int
Return values
ProductTag

tryDelete()

private tryDelete(callable(): bool $delete) : bool
Parameters
$delete : callable(): bool
Return values
bool

writeAccessoryCollection()

Writes the `accessories` of one set (merge/replace) with its own `find()` and `save()` using `includes: ['accessories']`. Mutates the collection in place (`add()`/`removeElement()`).

private writeAccessoryCollection(int $setId, string $mode, array<int, true> $addIds, array<int, true> $removeIds, array{code: string|null, action: string, line: int} $entry, ImportExecutionContext $context) : bool

IMPORTANT: never exchange the instance (the reused one would delete its row), and the includes must be complete because the save replaces the object graph.

Returns false when the collection was not written (set not found or save failed); the error is already logged and resolveAccessories() handles the summary and marker. Exceptions (find/save) propagate to the caller.

Parameters
$setId : int
$mode : string
$addIds : array<int, true>
$removeIds : array<int, true>
$entry : array{code: string|null, action: string, line: int}
$context : ImportExecutionContext
Return values
bool

writeImageAlts()

Writes alt texts (`<alt>` to `AssetText.name`). {@see AssetManager::updateColumn()} on `translations` is a DELETE and INSERT of all languages of the asset, so the existing rows are loaded first ({@see loadAssetTextRows()}) and only the languages `<alt>` mentions are overwritten; another language (or a `description` created outside the import) stays untouched.

private writeImageAlts(int $assetId, array<int, ImportValue> $alts, ImportExecutionContext $context) : bool

Returns false when the write failed (warning already logged); the caller propagates it to importIncomplete of the set.

Parameters
$assetId : int
$alts : array<int, ImportValue>
$context : ImportExecutionContext
Return values
bool
On this page

Search results