CoolCMS API

FeedXmlGenerator
in package

FinalYes

Generates the XML product feed (`<SHOP><SHOPITEM>…</SHOPITEM>…</SHOP>`, Heureka.cz style) from the parameter tree configured in the admin.

Actions are applied only through the map action.key => callable(ProductSet, ctx): mixed (actionMap()), never through an if/else chain. A closure may return a scalar or a list<scalar> (resolveActionResult()); a list makes sense only for multiple nodes, a non-multiple node takes its first item.

multiple=1 nodes (nested-set tree FeedParameter):

  • Manual repetitions come only from override rows (FeedParameterRel); a repetition without any non-empty value is skipped. Their count is the highest row count of the node and its children.
  • Automatic repetitions come only from actions: autoList[i] for a list result, or the same scalar in all repetitions; their count is the highest value count of the node and its children. A node without manual and automatic values is not rendered.
  • Children are paired by position (row i of the node + row i of each child = one repetition), so an empty override row of a child is stored as a placeholder; otherwise values would shift.

How manual and automatic values combine depends on the mode below.

Value mode (FeedParameterRel::$valueMode) belongs to the (parameter × product set) pair and is edited in the Feeds tab of the set; a node without override rows behaves as change.

  • change: a manual value replaces the automatic one; on a multiple node any manual value drops all automatic repetitions.
  • prepend / append: a simple node renders manual + ' ' + automatic / the reverse; a multiple node renders the manual repetitions before / after the automatic ones as two separate blocks (manual from override rows only, automatic from actions only; children pair by position within a block, otherwise PARAM_NAME/VAL pairs would fall apart). Joined strings use non-empty parts and are trimmed, otherwise <PRODUCTNAME>Samsung </PRODUCTNAME> gets a trailing space. A fully empty manual repetition is skipped (in change it is a positional placeholder).
  • none: the node is not rendered at all, unlike an empty field in change, which lets the automatic value through.
  • The mode of children is ignored (the parent creates the repetition); on a non-multiple node with children prepend/append behave as change.

getCategories applies the FeedProductCategoryRel mapping, otherwise the translated category name; several categories are joined with " | ".

Filter deleted=0 + activated=1 + publication window is the same condition as on the front end (ProductSetEligibilityFilters), built locally (the Api layer does not reach back into the Front namespace), so a deleted or unpublished set never ends up in the feed.

Table of Contents

Constants

BATCH_SIZE  : mixed = 200
Sets per batch. Each set carries about 23 includes, so this is smaller than for the sitemap; the query count per batch is constant (see {@see FEED_INCLUDES}).
FEED_INCLUDES  : array<int, string> = ['translations', 'product', 'product.producer',...
Everything any of the 23 actions ({@see actionMap()}) touches, loaded up front with one `findAll()` instead of lazily per item.

Properties

$currencyManager  : CurrencyManager
$em  : EntityManagerInterface
$feedManager  : FeedManager
$httpRequest  : IRequest
$languageManager  : LanguageManager
$linkGenerator  : LinkGenerator
$localizationManager  : LocalizationManager
$paymentRegionManager  : PaymentRegionManager
$productSetManager  : ProductSetManager
$settingContext  : SettingContext
$siteBaseUrl  : SiteBaseUrl|null
$unitOfWorkReset  : UnitOfWorkReset
$userGroupManager  : UserGroupManager

Methods

__construct()  : mixed
generate()  : array{path: string, itemCount: int}
Reads sets in batches by key ({@see KeysetBatches}) and writes the XML straight into a temporary file (`XMLWriter::openUri()`), calling `flush()` + `em->clear()` after each batch, so memory stays flat (the feed runs hourly).
generateById()  : array{path: string, itemCount: int}
absoluteAssetUrl()  : string|null
actionMap()  : array<string, callable(ProductSet, array<string, mixed>): mixed>
buildCategoriesText()  : string|null
buildProductUrl()  : string|null
IMPORTANT: the URL must carry `loc` explicitly, not only implicitly for the default language; otherwise a feed in English would link to the Czech detail. The link language is the feed language (page slug and set name).
collectParamIds()  : void
fetchProductSetBatch()  : array<int, ProductSet>
One batch of sets with `id > $afterId` (ascending by id). The feed limit (`itemLimit`) counts the rows of this query, i.e. before the filtering in {@see filterEligible()}.
filterEligible()  : array<int, ProductSet>
Bundles (`typeId=BUNDLE`) have no `product`; deleted/activated on `Product` applies only to `typeId=PRODUCT` (parity with Front `ProductSetEligibilityFilters::isEligible()`).
hasManualValue()  : bool
Whether repetition `$index` has at least one non-empty manual value. It decides whether a manual repetition is rendered at all, and in `change` whether the manual values replace the automatic ones.
loadActionKeys()  : array<int, string>
loadCategoryValues()  : array<int, string>
loadOverrides()  : array<int, array<int, array{rows: list, mode: string}>>
Override rows of all feed sets in one query (batch, not per item).
pickRepetitionValue()  : string
Value of repetition `$index` for a `multiple` node (or its child): the override row when `$useOverride`, otherwise the automatic value, taken by index when the action returned a list, otherwise the same scalar in all repetitions.
publishFeedFile()  : void
renderMultipleNode()  : void
A `multiple` node, i.e. an array of XML nodes. In `change` mode any manual value replaces all automatic ones; in `prepend`/`append` there are two separate blocks (manual, automatic) in that order. A node without overrides and without action values is not rendered at all.
renderNode()  : void
resolveActionResult()  : array{values: list, isList: bool}
Calls the action of the node and normalizes the result. `isList` says whether the action returned a list (values are paired by repetition index) or a scalar (broadcast to all repetitions), see {@see pickRepetitionValue()}.
resolveFeedPath()  : string
resolveLocaleKey()  : string|null
Locale key (`loc` of the route) for the feed language: the main localization first, otherwise the first one with this language; `null` = the default route language.
resolveNodeText()  : string
Text of a non-multiple node without children. `change`: the override wins, otherwise the action; `prepend`/`append`: the two strings joined by a space.
resolveProduct()  : Product|null
Product of the selling card. The physical variant (and through it the product) is held by `ProductSet.variant`; the direct `ProductSet.product` is an optional denormalization filled for only part of the sets. Without this fallback `getManufacturer`/`getCategories`/`getProductId`/ `getProductName`/`getProductImageUrl` returned nothing silently for the other sets (valid XML with empty content, so neither xmllint nor the harness catches it).
scalarToText()  : string|null
sortedAccessories()  : array<int, ProductSetAccessory>
Accessories of the set ordered by `order`: the common source for `getAccessoryId` and `getAccessoryName` (identical indexes, see {@see actionMap()}).
writeRepetition()  : void
One repetition of a `multiple` node: a node without children carries text, a node with children one element per child. `$useOverride`/`$useAction` select the value source; callers always pass exactly one of them, because manual and automatic repetitions are disjoint blocks in every mode.

Constants

BATCH_SIZE

Sets per batch. Each set carries about 23 includes, so this is smaller than for the sitemap; the query count per batch is constant (see {@see FEED_INCLUDES}).

private mixed BATCH_SIZE = 200

FEED_INCLUDES

Everything any of the 23 actions ({@see actionMap()}) touches, loaded up front with one `findAll()` instead of lazily per item.

private array<int, string> FEED_INCLUDES = ['translations', 'product', 'product.producer', 'product.asset', 'product.translations', 'product.categories', 'product.categories.translations', 'variant', 'variant.product', 'variant.product.producer', 'variant.product.asset', 'variant.product.translations', 'variant.product.categories', 'variant.product.categories.translations', 'assetRels', 'assetRels.asset', 'links', 'links.translations', 'links.linkType', 'links.linkType.translations', 'accessories', 'accessories.accessorySet', 'accessories.accessorySet.translations']

IMPORTANT: without it the generator issued about 3 SQL queries per item (links, images, categories and their translations, producer). With these includes the query count is constant (about 8) regardless of catalog size.

OrmRepository::splitIncludesByCardinality() splits the paths itself: ToOne stays a JOIN in the main query, ToMany is fetched by separate batch queries (no cartesian product). The variant.product.* branches serve the resolveProduct() fallback: part of the sets reach their product only through the variant.

Properties

Methods

__construct()

public __construct(EntityManagerInterface $em, UnitOfWorkReset $unitOfWorkReset, FeedManager $feedManager, ProductSetManager $productSetManager, CurrencyManager $currencyManager, LanguageManager $languageManager, UserGroupManager $userGroupManager, PaymentRegionManager $paymentRegionManager, LinkGenerator $linkGenerator, IRequest $httpRequest, SettingContext $settingContext, LocalizationManager $localizationManager[, SiteBaseUrl|null $siteBaseUrl = null ]) : mixed
Parameters
$em : EntityManagerInterface
$unitOfWorkReset : UnitOfWorkReset
$feedManager : FeedManager
$productSetManager : ProductSetManager
$currencyManager : CurrencyManager
$languageManager : LanguageManager
$userGroupManager : UserGroupManager
$paymentRegionManager : PaymentRegionManager
$linkGenerator : LinkGenerator
$httpRequest : IRequest
$settingContext : SettingContext
$localizationManager : LocalizationManager
$siteBaseUrl : SiteBaseUrl|null = null

generate()

Reads sets in batches by key ({@see KeysetBatches}) and writes the XML straight into a temporary file (`XMLWriter::openUri()`), calling `flush()` + `em->clear()` after each batch, so memory stays flat (the feed runs hourly).

public generate(Feed $feed) : array{path: string, itemCount: int}

IMPORTANT: after the first batch $feed is a detached entity (em->clear()), so everything needed from it is read up front, and nobody may call flush() on entities loaded before the generation.

Parameters
$feed : Feed
Return values
array{path: string, itemCount: int}

generateById()

public generateById(int $feedId) : array{path: string, itemCount: int}
Parameters
$feedId : int
Return values
array{path: string, itemCount: int}

absoluteAssetUrl()

private absoluteAssetUrl(object $asset, string $baseUrl) : string|null
Parameters
$asset : object
$baseUrl : string
Return values
string|null

actionMap()

private actionMap(int $languageId, int $currencyId[, string|null $localeKey = null ]) : array<string, callable(ProductSet, array<string, mixed>): mixed>
Parameters
$languageId : int
$currencyId : int
$localeKey : string|null = null
Return values
array<string, callable(ProductSet, array<string, mixed>): mixed>

buildCategoriesText()

private buildCategoriesText(ProductSet $set, int $languageId, array<int, string> $categoryValues) : string|null
Parameters
$set : ProductSet
$languageId : int
$categoryValues : array<int, string>

categoryId => mapped value

Return values
string|null

buildProductUrl()

IMPORTANT: the URL must carry `loc` explicitly, not only implicitly for the default language; otherwise a feed in English would link to the Czech detail. The link language is the feed language (page slug and set name).

private buildProductUrl(ProductSet $set[, string|null $localeKey = null ]) : string|null
Parameters
$set : ProductSet
$localeKey : string|null = null
Return values
string|null

collectParamIds()

private collectParamIds(array<string|int, mixed> $nodes, array<int, int> &$paramIds) : void
Parameters
$nodes : array<string|int, mixed>
$paramIds : array<int, int>

fetchProductSetBatch()

One batch of sets with `id > $afterId` (ascending by id). The feed limit (`itemLimit`) counts the rows of this query, i.e. before the filtering in {@see filterEligible()}.

private fetchProductSetBatch(int $afterId, int $limit) : array<int, ProductSet>
Parameters
$afterId : int
$limit : int
Return values
array<int, ProductSet>

filterEligible()

Bundles (`typeId=BUNDLE`) have no `product`; deleted/activated on `Product` applies only to `typeId=PRODUCT` (parity with Front `ProductSetEligibilityFilters::isEligible()`).

private filterEligible(array<int, ProductSet> $sets) : array<int, ProductSet>
Parameters
$sets : array<int, ProductSet>
Return values
array<int, ProductSet>

hasManualValue()

Whether repetition `$index` has at least one non-empty manual value. It decides whether a manual repetition is rendered at all, and in `change` whether the manual values replace the automatic ones.

private hasManualValue(array<int, array{tag: string, rows: list, result: array{values: list, isList: bool}}> $childData, array<int, string|null> $rows, int $index) : bool
Parameters
$childData : array<int, array{tag: string, rows: list, result: array{values: list, isList: bool}}>
$rows : array<int, string|null>
$index : int
Return values
bool

loadActionKeys()

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

feedParameterActionId => action string

loadCategoryValues()

private loadCategoryValues(int $feedId) : array<int, string>
Parameters
$feedId : int
Return values
array<int, string> —

categoryId => mapped value

loadOverrides()

Override rows of all feed sets in one query (batch, not per item).

private loadOverrides(array<int, array<string, mixed>> $tree, array<int, int> $setIds) : array<int, array<int, array{rows: list, mode: string}>>

IMPORTANT: the mode is authoritative from the first row of the (parameter, set) pair, i.e. the lowest r.id. The UI (Feeds tab) has one select per node, so all rows of a pair carry the same value; if they diverge anyway (manual DB edit, import), the oldest decides, deterministically and without another query.

Parameters
$tree : array<int, array<string, mixed>>
$setIds : array<int, int>
Return values
array<int, array<int, array{rows: list, mode: string}>> —

feedParameterId => productSetId

pickRepetitionValue()

Value of repetition `$index` for a `multiple` node (or its child): the override row when `$useOverride`, otherwise the automatic value, taken by index when the action returned a list, otherwise the same scalar in all repetitions.

private pickRepetitionValue(array<int, string|null> $rows, array{values: list, isList: bool} $actionResult, int $index[, bool $useOverride = true ][, bool $useAction = true ]) : string

IMPORTANT: an empty override row ('') is a positional placeholder that only keeps the index; in a manual repetition it renders an empty element, it is not filled from the action. Leaving the node out entirely is possible only with mode none.

Parameters
$rows : array<int, string|null>
$actionResult : array{values: list, isList: bool}
$index : int
$useOverride : bool = true
$useAction : bool = true
Return values
string

publishFeedFile()

private publishFeedFile(string $tmp, string $path) : void
Parameters
$tmp : string
$path : string

renderMultipleNode()

A `multiple` node, i.e. an array of XML nodes. In `change` mode any manual value replaces all automatic ones; in `prepend`/`append` there are two separate blocks (manual, automatic) in that order. A node without overrides and without action values is not rendered at all.

private renderMultipleNode(XMLWriter $writer, string $tag, array<string, mixed> $node, array<int, array<string, mixed>> $children, array<int, string|null> $rows, string $mode, ProductSet $set, array<string, mixed> $ctx, array<int, array<int, array{rows: list, mode: string}>> $overrides, array<int, string> $actionIdToKey, array<string, callable(ProductSet, array<string, mixed>): mixed> $actionMap) : void
Parameters
$writer : XMLWriter
$tag : string
$node : array<string, mixed>
$children : array<int, array<string, mixed>>
$rows : array<int, string|null>
$mode : string
$set : ProductSet
$ctx : array<string, mixed>
$overrides : array<int, array<int, array{rows: list, mode: string}>>
$actionIdToKey : array<int, string>
$actionMap : array<string, callable(ProductSet, array<string, mixed>): mixed>

renderNode()

private renderNode(XMLWriter $writer, array<string, mixed> $node, ProductSet $set, array<string, mixed> $ctx, array<int, array<int, array{rows: list, mode: string}>> $overrides, array<int, string> $actionIdToKey, array<string, callable(ProductSet, array<string, mixed>): mixed> $actionMap) : void
Parameters
$writer : XMLWriter
$node : array<string, mixed>
$set : ProductSet
$ctx : array<string, mixed>
$overrides : array<int, array<int, array{rows: list, mode: string}>>

feedParameterId => productSetId => override rows + mode

$actionIdToKey : array<int, string>
$actionMap : array<string, callable(ProductSet, array<string, mixed>): mixed>

resolveActionResult()

Calls the action of the node and normalizes the result. `isList` says whether the action returned a list (values are paired by repetition index) or a scalar (broadcast to all repetitions), see {@see pickRepetitionValue()}.

private resolveActionResult(array<string, mixed> $node, ProductSet $set, array<string, mixed> $ctx, array<int, string> $actionIdToKey, array<string, callable(ProductSet, array<string, mixed>): mixed> $actionMap) : array{values: list, isList: bool}
Parameters
$node : array<string, mixed>
$set : ProductSet
$ctx : array<string, mixed>
$actionIdToKey : array<int, string>
$actionMap : array<string, callable(ProductSet, array<string, mixed>): mixed>
Return values
array{values: list, isList: bool}

resolveFeedPath()

private resolveFeedPath(string $urlName) : string
Parameters
$urlName : string
Return values
string

resolveLocaleKey()

Locale key (`loc` of the route) for the feed language: the main localization first, otherwise the first one with this language; `null` = the default route language.

private resolveLocaleKey(int $languageId) : string|null
Parameters
$languageId : int
Return values
string|null

resolveNodeText()

Text of a non-multiple node without children. `change`: the override wins, otherwise the action; `prepend`/`append`: the two strings joined by a space.

private resolveNodeText(array<string, mixed> $node, string|null $override, string $mode, ProductSet $set, array<string, mixed> $ctx, array<int, string> $actionIdToKey, array<string, callable(ProductSet, array<string, mixed>): mixed> $actionMap) : string

IMPORTANT: only non-empty parts are joined and the result is trimmed; otherwise an empty manual value with append would produce <PRODUCTNAME>Samsung </PRODUCTNAME> with a trailing space.

When the action returns a list (meaningful only for multiple nodes), its first item is used.

Parameters
$node : array<string, mixed>
$override : string|null
$mode : string
$set : ProductSet
$ctx : array<string, mixed>
$actionIdToKey : array<int, string>
$actionMap : array<string, callable(ProductSet, array<string, mixed>): mixed>
Return values
string

resolveProduct()

Product of the selling card. The physical variant (and through it the product) is held by `ProductSet.variant`; the direct `ProductSet.product` is an optional denormalization filled for only part of the sets. Without this fallback `getManufacturer`/`getCategories`/`getProductId`/ `getProductName`/`getProductImageUrl` returned nothing silently for the other sets (valid XML with empty content, so neither xmllint nor the harness catches it).

private resolveProduct(ProductSet $set) : Product|null

NOTE: eligibility (filterEligible()) intentionally does not use this fallback: it keeps literal parity with the front end (ProductSetEligibilityFilters::isEligible()), which also checks only the direct getProduct(). Unifying both (front end and feed) is a separate change, not a silent deviation of the feed.

Parameters
$set : ProductSet
Return values
Product|null

scalarToText()

private scalarToText(mixed $value) : string|null
Parameters
$value : mixed
Return values
string|null

sortedAccessories()

Accessories of the set ordered by `order`: the common source for `getAccessoryId` and `getAccessoryName` (identical indexes, see {@see actionMap()}).

private sortedAccessories(ProductSet $set) : array<int, ProductSetAccessory>

IMPORTANT: no SQL here. The collection and its accessorySet.translations are in FEED_INCLUDES, so they arrive already hydrated (batch). A findBy/DQL over ProductSet in the generator would bypass batching and cause N+1.

Parameters
$set : ProductSet
Return values
array<int, ProductSetAccessory>

writeRepetition()

One repetition of a `multiple` node: a node without children carries text, a node with children one element per child. `$useOverride`/`$useAction` select the value source; callers always pass exactly one of them, because manual and automatic repetitions are disjoint blocks in every mode.

private writeRepetition(XMLWriter $writer, string $tag, array<int, array{tag: string, rows: list, result: array{values: list, isList: bool}}> $childData, array<int, string|null> $rows, array{values: list, isList: bool} $selfResult, int $index, bool $useOverride, bool $useAction) : void
Parameters
$writer : XMLWriter
$tag : string
$childData : array<int, array{tag: string, rows: list, result: array{values: list, isList: bool}}>
$rows : array<int, string|null>
$selfResult : array{values: list, isList: bool}
$index : int
$useOverride : bool
$useAction : bool
On this page

Search results