XmlImportParser
in package
implements
ImportParserInterface
Streaming parser of the canonical import XML that yields {@see ImportDto}.
It uses XMLReader, not SimpleXML/DOM of the whole file, because XML files up to 100 MB are
allowed and simplexml_load_file() would build a gigabyte tree. The parser reads forward and
expands to DOM only one <product> card at a time (XMLReader::expand()), yields it
from a generator and forgets it, so memory stays constant. expand() is also the only way to
get the line number (DOMNode::getLineNo()) that the per-line log needs.
IMPORTANT: XXE protection has three independent safeguards: LIBXML_NONET (no network), LOADDTD
and SUBST_ENTITIES off (an entity is never substituted), and immediate rejection of any
DOCTYPE (XMLReader::DOC_TYPE) or entity reference (XMLReader::ENTITY_REF). The import
needs no foreign DTD, so its presence is itself an attack signal.
Errors: a defect that cannot be attributed to an item (version, DTD, encoding, broken XML, an
unknown tag above <product>) is thrown as ImportParseException and the job ends as
invalid. A defect inside a card (unknown tag/attribute, reserved tag) is wrapped in
ImportStructureIssueDto and carried in that card's DTO as a hard error of one item; the
rest of the file is still validated.
The parser sanitizes and converts nothing: the CDATA description goes to the DTO as is (editor sanitization belongs to the executor), and booleans, numbers and dates stay strings (the validator interprets them).
Table of Contents
Interfaces
- ImportParserInterface
- Common interface of the three format parsers; all fill one `ImportDto`, so validator and executor are format-independent. {@see XmlImportParser}, {@see CsvImportParser} and {@see JsonImportParser} implement it; {@see ImportParserFactory} selects the implementation by `ProductImportFormat`.
Constants
- SUPPORTED_VERSION : string = '1.0'
- BUNDLE_SCALAR_TAGS : array<string|int, mixed> = ['activated', 'vat']
- BUNDLE_TEXT_TAGS : array<string|int, mixed> = ['name', 'longName', 'shortDescription', 'descr...
- PRICE_VALUE_TAGS : array<string|int, mixed> = ['amount', 'amountWithVat', 'originalAmount', '...
- PRODUCT_SCALAR_TAGS : array<string|int, mixed> = ['producer', 'productType', 'activated']
- PRODUCT_TEXT_TAGS : array<string|int, mixed> = ['name', 'description']
- RESERVED_VARIANT_TAGS : array<string|int, mixed> = ['links', 'fees', 'vats']
- Reserved tags inside `<variant>`.
- SETTINGS_TAGS : array<string|int, mixed> = ['mode', 'matchBy', 'defaultLang', 'collectionM...
- VARIANT_SCALAR_TAGS : array<string|int, mixed> = ['code', 'ean', 'isbn', 'weight', 'warranty', '...
- VARIANT_TEXT_TAGS : array<string|int, mixed> = ['name', 'longName', 'shortDescription', 'descr...
Methods
- parse() : ImportDto
- assertNoLibxmlError() : void
- Handles libxml errors (broken XML, schema failure); the first one fails the whole file.
- assertUtf8Declaration() : void
- Requires the declared encoding to be UTF-8.
- buildAccessories() : ImportCollectionDto<string|int, ImportAccessoryDto>
- buildBundle() : ImportBundleDto
- Reads `<bundle code="" id="" action="">`; unlike a variant, `code` is an attribute.
- buildBundleItems() : array<int, ImportBundleItemDto>
- Reads the bundle composition `<items><item code="" count=""/></items>`.
- buildBundlesBlock() : void
- buildCategories() : ImportCollectionDto<string|int, ImportCategoryDto>
- buildImages() : ImportCollectionDto<string|int, ImportImageDto>
- buildParameters() : ImportCollectionDto<string|int, ImportParameterDto>
- buildPrices() : array<int, ImportPriceDto>
- buildProduct() : ImportProductDto
- buildStock() : ImportStockDto
- buildTags() : ImportCollectionDto<string|int, ImportTagDto>
- buildValue() : ImportValue|null
- Reads the node text as is (CDATA is not sanitized) together with `lang` and the line number.
- buildVariant() : ImportVariantDto
- childElements() : array<int, DOMElement>
- Returns the expected collection items; anything else inside the wrapper is an unknown tag.
- collectUnknownAttributes() : void
- expandElement() : DOMElement
- finishEmpty() : Generator<int, ImportProductDto>
- Handles a file without `<products>`: zero items, but the rest of the envelope (`<bundles>`, reserved tags) must still be read as for a full file.
- iterateProducts() : Generator<int, ImportProductDto>
- Yields cards with only one `<product>` in memory at a time.
- readActionAttribute() : string|null
- readBoolAttribute() : bool
- Reads the boolean attributes `remove`/`main`: canonically `1/0`, `true/false` is accepted too.
- readCollectionMode() : string|null
- readEnvelope() : string
- Reads the envelope, the root `<import version="1.0">`; the reader stands on the root afterwards.
- readEpilogue() : void
- Reads everything after `</products>` up to `</import>`.
- readIntAttribute() : int|null
- Reads an integer attribute; a missing attribute is `null`.
- readPrelude() : ImportSettingsDto|null
- Reads everything between `<import>` and `<products>`, that is the optional `<settings>`.
- readSettings() : ImportSettingsDto
- readStringAttribute() : string|null
Constants
SUPPORTED_VERSION
public
string
SUPPORTED_VERSION
= '1.0'
BUNDLE_SCALAR_TAGS
private
array<string|int, mixed>
BUNDLE_SCALAR_TAGS
= ['activated', 'vat']
bundle scalar tags, deliberately narrower than a variant (a bundle has no stock or parameters)
BUNDLE_TEXT_TAGS
private
array<string|int, mixed>
BUNDLE_TEXT_TAGS
= ['name', 'longName', 'shortDescription', 'description']
bundle text tags (repeatable per lang), the same set as for a variant
PRICE_VALUE_TAGS
private
array<string|int, mixed>
PRICE_VALUE_TAGS
= ['amount', 'amountWithVat', 'originalAmount', 'originalAmountWithVat', 'points']
price value tags (price pair plus accompanying ones)
PRODUCT_SCALAR_TAGS
private
array<string|int, mixed>
PRODUCT_SCALAR_TAGS
= ['producer', 'productType', 'activated']
product card scalar tags
PRODUCT_TEXT_TAGS
private
array<string|int, mixed>
PRODUCT_TEXT_TAGS
= ['name', 'description']
product card text tags (repeatable per lang)
RESERVED_VARIANT_TAGS
Reserved tags inside `<variant>`.
private
array<string|int, mixed>
RESERVED_VARIANT_TAGS
= ['links', 'fees', 'vats']
SETTINGS_TAGS
private
array<string|int, mixed>
SETTINGS_TAGS
= ['mode', 'matchBy', 'defaultLang', 'collectionMode', 'autoCreateProducers', 'autoCreateOptions', 'autoCreateCategories', 'stopOnError']
the only tags <settings> may contain
VARIANT_SCALAR_TAGS
private
array<string|int, mixed>
VARIANT_SCALAR_TAGS
= ['code', 'ean', 'isbn', 'weight', 'warranty', 'freeDelivery', 'freePayment', 'publishTime', 'unpublishTime', 'activated', 'vat', 'state']
variant scalar tags
VARIANT_TEXT_TAGS
private
array<string|int, mixed>
VARIANT_TEXT_TAGS
= ['name', 'longName', 'shortDescription', 'description']
variant text tags (repeatable per lang)
Methods
parse()
public
parse(string $absolutePath[, string|null $schemaPath = null ]) : ImportDto
Parameters
- $absolutePath : string
-
absolute file path (never from the request, always the job's
filePath) - $schemaPath : string|null = null
-
optional
xsdshape check;nullmeans the PHP grammar only. The schema checks structure and types, business rules belong to the validator.
Tags
Return values
ImportDtoassertNoLibxmlError()
Handles libxml errors (broken XML, schema failure); the first one fails the whole file.
private
assertNoLibxmlError() : void
assertUtf8Declaration()
Requires the declared encoding to be UTF-8.
private
assertUtf8Declaration(string $absolutePath) : void
Only the file prolog is read; otherwise one header would pull 100 MB into memory.
Parameters
- $absolutePath : string
buildAccessories()
private
buildAccessories(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportCollectionDto<string|int, ImportAccessoryDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportCollectionDto<string|int, ImportAccessoryDto>buildBundle()
Reads `<bundle code="" id="" action="">`; unlike a variant, `code` is an attribute.
private
buildBundle(DOMElement $element) : ImportBundleDto
Parameters
- $element : DOMElement
Return values
ImportBundleDtobuildBundleItems()
Reads the bundle composition `<items><item code="" count=""/></items>`.
private
buildBundleItems(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : array<int, ImportBundleItemDto>
Like <accessories>, but with a required count instead of remove.
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
array<int, ImportBundleItemDto>buildBundlesBlock()
private
buildBundlesBlock(DOMElement $element, ImportBundleBag $bundleBag, ImportIssueBag $rootIssues) : void
Parameters
- $element : DOMElement
- $bundleBag : ImportBundleBag
- $rootIssues : ImportIssueBag
-
an unknown sibling inside
<bundles>is a defect above the level of one item
buildCategories()
private
buildCategories(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportCollectionDto<string|int, ImportCategoryDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportCollectionDto<string|int, ImportCategoryDto>buildImages()
private
buildImages(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportCollectionDto<string|int, ImportImageDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportCollectionDto<string|int, ImportImageDto>buildParameters()
private
buildParameters(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportCollectionDto<string|int, ImportParameterDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportCollectionDto<string|int, ImportParameterDto>buildPrices()
private
buildPrices(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : array<int, ImportPriceDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
array<int, ImportPriceDto>buildProduct()
private
buildProduct(DOMElement $element) : ImportProductDto
Parameters
- $element : DOMElement
Return values
ImportProductDtobuildStock()
private
buildStock(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportStockDto
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportStockDtobuildTags()
private
buildTags(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportCollectionDto<string|int, ImportTagDto>
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportCollectionDto<string|int, ImportTagDto>buildValue()
Reads the node text as is (CDATA is not sanitized) together with `lang` and the line number.
private
buildValue(DOMElement $element, array<int, ImportStructureIssueDto> &$issues) : ImportValue|null
IMPORTANT: a scalar node must not have element children. textContent includes them, so
<ean><foo/></ean> gave an empty value, which means "clear" in tri-state semantics, and a
valid update silently erased the EAN with no log entry; <weight><a>5</a><b>7</b></weight>
gave the concatenation 57. Like JsonImportParser::readScalarValue(), a malformed
shape is KIND_MALFORMED, the value is not written and the issue blocks the item.
The XSD would catch it too, but production ProductImportManager calls parse()
without a schema, so the gate must be here.
Parameters
- $element : DOMElement
- $issues : array<int, ImportStructureIssueDto>
Return values
ImportValue|null —null for a malformed shape; the value is dropped and the issue blocks the item
buildVariant()
private
buildVariant(DOMElement $element) : ImportVariantDto
Parameters
- $element : DOMElement
Return values
ImportVariantDtochildElements()
Returns the expected collection items; anything else inside the wrapper is an unknown tag.
private
childElements(DOMElement $element, string $expected, array<int, ImportStructureIssueDto> &$issues) : array<int, DOMElement>
Parameters
- $element : DOMElement
- $expected : string
- $issues : array<int, ImportStructureIssueDto>
Return values
array<int, DOMElement>collectUnknownAttributes()
private
collectUnknownAttributes(DOMElement $element, array<int, string> $allowed, array<int, ImportStructureIssueDto> &$issues) : void
Parameters
- $element : DOMElement
- $allowed : array<int, string>
- $issues : array<int, ImportStructureIssueDto>
expandElement()
private
expandElement(XMLReader $reader) : DOMElement
Parameters
- $reader : XMLReader
Return values
DOMElementfinishEmpty()
Handles a file without `<products>`: zero items, but the rest of the envelope (`<bundles>`, reserved tags) must still be read as for a full file.
private
finishEmpty(XMLReader $reader, ImportIssueBag $rootIssues, ImportBundleBag $bundleBag) : Generator<int, ImportProductDto>
Parameters
- $reader : XMLReader
- $rootIssues : ImportIssueBag
- $bundleBag : ImportBundleBag
Return values
Generator<int, ImportProductDto>iterateProducts()
Yields cards with only one `<product>` in memory at a time.
private
iterateProducts(XMLReader $reader, ImportIssueBag $rootIssues, ImportBundleBag $bundleBag) : Generator<int, ImportProductDto>
When exhausted it reads the rest of the file (for <bundles> after </products>) and closes the reader.
Parameters
- $reader : XMLReader
- $rootIssues : ImportIssueBag
- $bundleBag : ImportBundleBag
Return values
Generator<int, ImportProductDto>readActionAttribute()
private
readActionAttribute(DOMElement $element) : string|null
Parameters
- $element : DOMElement
Return values
string|nullreadBoolAttribute()
Reads the boolean attributes `remove`/`main`: canonically `1/0`, `true/false` is accepted too.
private
readBoolAttribute(DOMElement $element, string $name) : bool
Parameters
- $element : DOMElement
- $name : string
Return values
boolreadCollectionMode()
private
readCollectionMode(DOMElement $element) : string|null
Parameters
- $element : DOMElement
Return values
string|nullreadEnvelope()
Reads the envelope, the root `<import version="1.0">`; the reader stands on the root afterwards.
private
readEnvelope(XMLReader $reader) : string
Parameters
- $reader : XMLReader
Return values
stringreadEpilogue()
Reads everything after `</products>` up to `</import>`.
private
readEpilogue(XMLReader $reader, ImportIssueBag $rootIssues, ImportBundleBag $bundleBag) : void
<bundles> (see buildBundlesBlock()) is the only possible content. <links>, <fees>
and <vats> stay reserved only inside <variant> (RESERVED_VARIANT_TAGS); there is no
reserved tag at the <import> level.
Parameters
- $reader : XMLReader
- $rootIssues : ImportIssueBag
- $bundleBag : ImportBundleBag
readIntAttribute()
Reads an integer attribute; a missing attribute is `null`.
private
readIntAttribute(DOMElement $element, string $name, array<int, ImportStructureIssueDto> &$issues, string $context[, bool $allowZero = false ]) : int|null
A present but invalid one (1e2, -5, 0, inner spaces, overflow past PHP_INT_MAX
that (int) would saturate) is not "missing": it records KIND_MALFORMED and returns
null, so the item ends up blocked. An unknown target must never silently turn into
"create a new one".
Parameters
- $element : DOMElement
- $name : string
- $issues : array<int, ImportStructureIssueDto>
- $context : string
- $allowZero : bool = false
Return values
int|nullreadPrelude()
Reads everything between `<import>` and `<products>`, that is the optional `<settings>`.
private
readPrelude(XMLReader $reader, bool &$hasProducts, ImportBundleBag $bundleBag, ImportIssueBag $rootIssues) : ImportSettingsDto|null
Afterwards the reader stands on the opening <products> ($hasProducts = true) or the file
has ended. <bundles> may appear without <products> (a file importing only bundles); it is
handled here with the same logic as readEpilogue(), because no <products> follows.
Parameters
- $reader : XMLReader
- $hasProducts : bool
- $bundleBag : ImportBundleBag
- $rootIssues : ImportIssueBag
Return values
ImportSettingsDto|nullreadSettings()
private
readSettings(XMLReader $reader) : ImportSettingsDto
Parameters
- $reader : XMLReader
Return values
ImportSettingsDtoreadStringAttribute()
private
readStringAttribute(DOMElement $element, string $name) : string|null
Parameters
- $element : DOMElement
- $name : string