CoolCMS API

XmlImportParser
in package
implements ImportParserInterface

FinalYes

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

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 xsd shape check; null means the PHP grammar only. The schema checks structure and types, business rules belong to the validator.

Tags
throws
ImportParseException

when the file as a whole is unusable (the job becomes invalid)

Return values
ImportDto

assertNoLibxmlError()

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

buildBundle()

Reads `<bundle code="" id="" action="">`; unlike a variant, `code` is an attribute.

private buildBundle(DOMElement $element) : ImportBundleDto
Parameters
$element : DOMElement
Return values
ImportBundleDto

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

childElements()

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>

expandElement()

private expandElement(XMLReader $reader) : DOMElement
Parameters
$reader : XMLReader
Return values
DOMElement

readActionAttribute()

private readActionAttribute(DOMElement $element) : string|null
Parameters
$element : DOMElement
Return values
string|null

readBoolAttribute()

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
bool

readCollectionMode()

private readCollectionMode(DOMElement $element) : string|null
Parameters
$element : DOMElement
Return values
string|null

readEnvelope()

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
string

readEpilogue()

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|null

readPrelude()

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|null

readStringAttribute()

private readStringAttribute(DOMElement $element, string $name) : string|null
Parameters
$element : DOMElement
$name : string
Return values
string|null
On this page

Search results