CsvImportParser
in package
implements
ImportParserInterface
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).
DELIMITER_ALT
private
string
DELIMITER_ALT
= ','
DELIMITER_DEFAULT
private
string
DELIMITER_DEFAULT
= ';'
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
ImportDtobuildImages()
Parses `image1`...`image5` (order = column order); CSV has no `main`, `assetId` or alt texts.
private
buildImages(array<string, string> $cells, int $line) : ImportCollectionDto<string|int, ImportImageDto>|null
Parameters
- $cells : array<string, string>
- $line : int
Return values
ImportCollectionDto<string|int, ImportImageDto>|nullbuildMultiValueCollection()
`#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
Return values
ImportCollectionDto<string|int, T>|nullbuildParameters()
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>|nullbuildPrices()
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>buildProduct()
private
buildProduct(array<int, array{line: int, cells: array}> $group) : ImportProductDto
Parameters
-
$group
: array<int, array{line: int, cells: array
}>
Return values
ImportProductDtobuildStock()
private
buildStock(array<string, string> $cells, int $line) : ImportStockDto|null
Parameters
- $cells : array<string, string>
- $line : int
Return values
ImportStockDto|nullbuildValue()
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|nullbuildVariant()
private
buildVariant(array{line: int, cells: array} $row) : ImportVariantDto
Parameters
-
$row
: array{line: int, cells: array
}
Return values
ImportVariantDtodetectDelimiter()
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
stringhasColumn()
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
boolmapRowToCells()
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>
yieldProducts()
private
yieldProducts(array<int, ImportProductDto> $products) : Generator
Parameters
- $products : array<int, ImportProductDto>