CoolCMS API

CsvImportParser
in package
implements ImportParserInterface

FinalYes

Parses the flat CSV subset into {@see ImportDto}, with the same output shape as {@see XmlImportParser}.

CSV has no bundles, so ImportDto::getBundles() is always empty for this parser.

One row is one variant. The product column groups rows into a card; a group need not be contiguous in the file. The parser reads the file as a stream (fgetcsv, no copy of the whole content), but must keep the cells of all rows until they are grouped (CSV max 20 MB, see MAX_BYTES). Groups are ordered by the first occurrence of the product token to keep the card order of the document. Card columns (productName:*, producer, productType, categories, productImage) are taken only from the first row of a group; a mismatch on a later row is a structural defect of the card (hard error, the whole card is not written), like an unknown XML tag.

Encoding: a BOM is stripped (unlike XML, where a BOM is a hard rejection). A file that is not valid UTF-8 is converted from Windows-1250 (common export of Czech Excel); success is reported to the rootIssues bag as info/warning, failure is a hard rejection of the file.

Delimiter: ; canonically, , is auto-detected from the header. The header is case-insensitive; an unknown column is a structural error of the whole file (the header applies to all rows, so a typo in it is a defect of the envelope, not of one item). param:<unknown> is not a structural error: an unknown parameter is a soft item error resolved by the validator against the code list that the parser does not know.

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

CARD_PREFIXES  : array<string|int, mixed> = ['producer', 'producttype', 'categories', 'prod...
DELIMITER_ALT  : string = ','
DELIMITER_DEFAULT  : string = ';'
FIXED_COLUMNS  : array<string|int, mixed> = ['code', 'product', 'action', 'ean', 'isbn', 'u...
MAX_BYTES  : int = 20 * 1024 * 1024
CSV/JSON limit of 20 MB; a defensive cap here too (the admin add form checks it earlier).
NULL_LITERAL  : string = '#null'
Literal for an explicit reset, case-insensitive.
PREFIXED_COLUMNS  : array<string|int, mixed> = ['name', 'longname', 'shortdescription', 'descr...
PRODUCT_SCALAR_MAP  : array<string|int, mixed> = ['producer' => 'producer', 'producttype' => 'pr...
PRODUCT_TEXT_MAP  : array<string|int, mixed> = ['productname' => 'name', 'productdescription'...
VARIANT_SCALAR_MAP  : array<string|int, mixed> = ['code' => 'code', 'ean' => 'ean', 'isbn' => 'i...
VARIANT_TEXT_MAP  : array<string|int, mixed> = ['name' => 'name', 'longname' => 'longName', 's...

Methods

parse()  : ImportDto
buildImages()  : ImportCollectionDto<string|int, ImportImageDto>|null
Parses `image1`...`image5` (order = column order); CSV has no `main`, `assetId` or alt texts.
buildMultiValueCollection()  : ImportCollectionDto<string|int, T>|null
`#NULL` on a collection column (categories/tags) has no XML equivalent (XML resets through `collectionMode="replace"` with empty content), so CSV maps it to `collectionMode=replace` with an empty item list: the collection is emptied regardless of the job settings, which matches the intuitive "reset".
buildParameters()  : ImportCollectionDto<string|int, ImportParameterDto>|null
Parses `param:<key>` columns: an empty cell means untouched, `#NULL` deletes the rows of the key, `from..to` is a range, `value1|value2` is a multiselect (N items), anything else is one text value stored in `text`, not `option`: CSV cannot write the internal option key, only its name (case-insensitive fallback {@see ImportCodebooks::findParameterOptionId()}).
buildPrices()  : array<int, ImportPriceDto>
Parses `price:<CUR>`, `priceWithVat:<CUR>`, `originalPrice:<CUR>` and `originalPriceWithVat:<CUR>` columns.
buildProduct()  : ImportProductDto
buildStock()  : ImportStockDto|null
buildValue()  : ImportValue|null
An empty cell is untouched (`null`), `#NULL` (case-insensitive) is a reset, anything else is the value 1:1 (no trimming; multiline CSV content in quotes is preserved).
buildVariant()  : ImportVariantDto
detectDelimiter()  : string
Detects the delimiter: `;` canonically, `,` by the header (first row of the file). Rewinds the stream to `$offset` after reading the row.
hasColumn()  : bool
mapRowToCells()  : array<string, string>
Maps positional row cells to `<prefix>[:<suffix>]` => raw value (case-insensitive header key, value 1:1 from the file; trim and tri-state are handled by {@see buildValue()}).
openStream()  : array{0: resource, 1: string}
Reads the file as a stream (`fgetcsv`), not whole into memory. A BOM is skipped. The encoding is verified row by row (`\n` never lies inside a multibyte UTF-8 sequence); a file that is not valid UTF-8 is converted row by row from Windows-1250 into `php://temp` (PHP spills it to disk above 2 MB) and read from there.
parseHeader()  : array<int, array{prefix: string, suffix: ?string, isCard: bool}>
readRows()  : Generator<int, array<int, string|null>>
Yields file rows one by one; `fgetcsv` returns `false` only at the end of the file. A blank row is `[null]` (row numbers in errors rely on nothing being skipped). Closes the stream at the end.
validateCardColumnsMatch()  : void
yieldProducts()  : Generator

Constants

CARD_PREFIXES

private array<string|int, mixed> CARD_PREFIXES = ['producer', 'producttype', 'categories', 'productname', 'productdescription', 'productimage']

columns whose value belongs to the card (taken only from the first row of a group).

FIXED_COLUMNS

private array<string|int, mixed> FIXED_COLUMNS = ['code', 'product', 'action', 'ean', 'isbn', 'unit', 'weight', 'warranty', 'vat', 'freedelivery', 'freepayment', 'activated', 'publishtime', 'unpublishtime', 'producer', 'producttype', 'state', 'categories', 'tags', 'stock', 'stockmode', 'minquantity', 'warehouse', 'image1', 'image2', 'image3', 'image4', 'image5', 'productimage']

columns without a suffix; the header is case-insensitive.

MAX_BYTES

CSV/JSON limit of 20 MB; a defensive cap here too (the admin add form checks it earlier).

private int MAX_BYTES = 20 * 1024 * 1024

NULL_LITERAL

Literal for an explicit reset, case-insensitive.

private string NULL_LITERAL = '#null'

PREFIXED_COLUMNS

private array<string|int, mixed> PREFIXED_COLUMNS = ['name', 'longname', 'shortdescription', 'description', 'productname', 'productdescription', 'param', 'price', 'pricewithvat', 'originalprice', 'originalpricewithvat']

column prefixes with a suffix after : (language, currency or parameter key).

PRODUCT_SCALAR_MAP

private array<string|int, mixed> PRODUCT_SCALAR_MAP = ['producer' => 'producer', 'producttype' => 'productType']

lowercase prefix without suffix mapped to the canonical field name of the card DTO.

PRODUCT_TEXT_MAP

private array<string|int, mixed> PRODUCT_TEXT_MAP = ['productname' => 'name', 'productdescription' => 'description']

lowercase prefix with suffix mapped to the canonical text field name of the card.

VARIANT_SCALAR_MAP

private array<string|int, mixed> VARIANT_SCALAR_MAP = ['code' => 'code', 'ean' => 'ean', 'isbn' => 'isbn', 'unit' => 'unit', 'weight' => 'weight', 'warranty' => 'warranty', 'vat' => 'vat', 'freedelivery' => 'freeDelivery', 'freepayment' => 'freePayment', 'activated' => 'activated', 'publishtime' => 'publishTime', 'unpublishtime' => 'unpublishTime', 'state' => 'state']

lowercase prefix without suffix mapped to the canonical field name of the variant DTO.

VARIANT_TEXT_MAP

private array<string|int, mixed> VARIANT_TEXT_MAP = ['name' => 'name', 'longname' => 'longName', 'shortdescription' => 'shortDescription', 'description' => 'description']

lowercase prefix with suffix mapped to the canonical text field name of the variant.

Methods

parse()

public parse(string $absolutePath[, string|null $schemaPath = null ]) : ImportDto
Parameters
$absolutePath : string

absolute path to the file (never from the request, always the job filePath)

$schemaPath : string|null = null

optional xsd shape check; only XML supports it, csv/json ignore the parameter

Return values
ImportDto

buildMultiValueCollection()

`#NULL` on a collection column (categories/tags) has no XML equivalent (XML resets through `collectionMode="replace"` with empty content), so CSV maps it to `collectionMode=replace` with an empty item list: the collection is emptied regardless of the job settings, which matches the intuitive "reset".

private buildMultiValueCollection(string $raw, int $line, callable(string, int): T $factory) : ImportCollectionDto<string|int, T>|null
Parameters
$raw : string
$line : int
$factory : callable(string, int): T
Tags
template
Return values
ImportCollectionDto<string|int, T>|null

buildParameters()

Parses `param:<key>` columns: an empty cell means untouched, `#NULL` deletes the rows of the key, `from..to` is a range, `value1|value2` is a multiselect (N items), anything else is one text value stored in `text`, not `option`: CSV cannot write the internal option key, only its name (case-insensitive fallback {@see ImportCodebooks::findParameterOptionId()}).

private buildParameters(array<string, string> $cells, int $line) : ImportCollectionDto<string|int, ImportParameterDto>|null
Parameters
$cells : array<string, string>
$line : int
Return values
ImportCollectionDto<string|int, ImportParameterDto>|null

buildPrices()

Parses `price:<CUR>`, `priceWithVat:<CUR>`, `originalPrice:<CUR>` and `originalPriceWithVat:<CUR>` columns.

private buildPrices(array<string, string> $cells, int $line) : array<int, ImportPriceDto>

CSV has no fromAmount or userGroupId column, so it always uses the base band (fromAmount=0, userGroupId=null = default group). The rule that only one of the price/priceWithVat pair may be set is evaluated by the validator exactly as for XML; the parser only passes on what the file holds (even when both are filled, the hard error arises in the dry run or execution).

Parameters
$cells : array<string, string>
$line : int
Return values
array<int, ImportPriceDto>

buildValue()

An empty cell is untouched (`null`), `#NULL` (case-insensitive) is a reset, anything else is the value 1:1 (no trimming; multiline CSV content in quotes is preserved).

private buildValue(string $raw, int $line[, string|null $lang = null ]) : ImportValue|null
Parameters
$raw : string
$line : int
$lang : string|null = null
Return values
ImportValue|null

detectDelimiter()

Detects the delimiter: `;` canonically, `,` by the header (first row of the file). Rewinds the stream to `$offset` after reading the row.

private detectDelimiter(resource $handle, int $offset) : string
Parameters
$handle : resource
$offset : int
Return values
string

hasColumn()

private hasColumn(array<int, array{prefix: string, suffix: ?string, isCard: bool}> $columns, string $prefix) : bool
Parameters
$columns : array<int, array{prefix: string, suffix: ?string, isCard: bool}>
$prefix : string
Return values
bool

mapRowToCells()

Maps positional row cells to `<prefix>[:<suffix>]` => raw value (case-insensitive header key, value 1:1 from the file; trim and tri-state are handled by {@see buildValue()}).

private mapRowToCells(array<int, string|null> $row, array<int, array{prefix: string, suffix: ?string, isCard: bool}> $columns, int $line) : array<string, string>
Parameters
$row : array<int, string|null>
$columns : array<int, array{prefix: string, suffix: ?string, isCard: bool}>
$line : int
Return values
array<string, string>

openStream()

Reads the file as a stream (`fgetcsv`), not whole into memory. A BOM is skipped. The encoding is verified row by row (`\n` never lies inside a multibyte UTF-8 sequence); a file that is not valid UTF-8 is converted row by row from Windows-1250 into `php://temp` (PHP spills it to disk above 2 MB) and read from there.

private openStream(string $absolutePath, ImportIssueBag $rootIssues) : array{0: resource, 1: string}
Parameters
$absolutePath : string
$rootIssues : ImportIssueBag
Return values
array{0: resource, 1: string} —

open stream positioned at the header start, and the delimiter

parseHeader()

private parseHeader(array<int, string|null> $headerRow) : array<int, array{prefix: string, suffix: ?string, isCard: bool}>
Parameters
$headerRow : array<int, string|null>
Return values
array<int, array{prefix: string, suffix: ?string, isCard: bool}> —

positional, matching the file columns

readRows()

Yields file rows one by one; `fgetcsv` returns `false` only at the end of the file. A blank row is `[null]` (row numbers in errors rely on nothing being skipped). Closes the stream at the end.

private readRows(resource $handle, string $delimiter) : Generator<int, array<int, string|null>>
Parameters
$handle : resource
$delimiter : string
Return values
Generator<int, array<int, string|null>>

validateCardColumnsMatch()

private validateCardColumnsMatch(array{line: int, cells: array} $cardRow, array{line: int, cells: array} $otherRow, array<int, ImportStructureIssueDto> &$issues) : void
Parameters
$cardRow : array{line: int, cells: array}
$otherRow : array{line: int, cells: array}
$issues : array<int, ImportStructureIssueDto>
On this page

Search results