FeedXmlGenerator
in package
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
iof the node + rowiof 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 amultiplenode any manual value drops all automatic repetitions.prepend/append: a simple node rendersmanual + ' ' + automatic/ the reverse; amultiplenode 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, otherwisePARAM_NAME/VALpairs 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 (inchangeit is a positional placeholder).none: the node is not rendered at all, unlike an empty field inchange, 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/appendbehave aschange.
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
$currencyManager read-only
private
CurrencyManager
$currencyManager
$em read-only
private
EntityManagerInterface
$em
$feedManager read-only
private
FeedManager
$feedManager
$httpRequest read-only
private
IRequest
$httpRequest
$languageManager read-only
private
LanguageManager
$languageManager
$linkGenerator read-only
private
LinkGenerator
$linkGenerator
$localizationManager read-only
private
LocalizationManager
$localizationManager
$paymentRegionManager read-only
private
PaymentRegionManager
$paymentRegionManager
$productSetManager read-only
private
ProductSetManager
$productSetManager
$settingContext read-only
private
SettingContext
$settingContext
$siteBaseUrl read-only
private
SiteBaseUrl|null
$siteBaseUrl
= null
$unitOfWorkReset read-only
private
UnitOfWorkReset
$unitOfWorkReset
$userGroupManager read-only
private
UserGroupManager
$userGroupManager
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|nullactionMap()
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|nullbuildProductUrl()
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|nullcollectParamIds()
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
boolloadActionKeys()
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
stringpublishFeedFile()
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: listresolveFeedPath()
private
resolveFeedPath(string $urlName) : string
Parameters
- $urlName : string
Return values
stringresolveLocaleKey()
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|nullresolveNodeText()
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
stringresolveProduct()
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|nullscalarToText()
private
scalarToText(mixed $value) : string|null
Parameters
- $value : mixed
Return values
string|nullsortedAccessories()
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