CoolCMS API

ImportValidator
in package

FinalYes

Dry-run validation of an import file: matching, limits, structure and code lists.

IMPORTANT: never writes anything to the catalog. Dev and production share one database, so the validator only reads and produces a report plus a row-per-line JSONL log.

A hard error blocks the item; a soft error writes the rest of the item and flags it importIncomplete.

Cards are buffered in LOOKUP_CHUNK batches and matched with one query each; code lists are loaded once per run as id maps (ImportCodebooks). The executor reuses the same rules through begin(), validateChunk() and finish(); limits live in ImportRules, matching in ImportMatcher.

Table of Contents

Constants

LOOKUP_CHUNK  : int = \App\UI\Api\Eshop\Models\Import\ImportMatcher::...
Cards per matching query, see {@see ImportMatcher::LOOKUP_CHUNK}.

Properties

$codebookLoader  : ImportCodebookLoader
$em  : EntityManagerInterface
$matcher  : ImportMatcher

Methods

__construct()  : mixed
begin()  : ImportValidationContext
Starts a run: loads code lists (once), compares `<settings>` of the file with the job and checks the default language. The returned context is passed to {@see validateChunk()} and {@see finish()}.
findOverlongPathPart()  : string|null
Returns the first segment of a path like `Elektro / Apple` that does not fit varchar(255). Shared with the writer so that the dry run promises what the write does.
finish()  : ImportValidationSummary
Ends a run: root issues (reserved tags, CSV encoding), accessory targets and the rewrite of log row counts into the summary.
validate()  : ImportValidationSummary
Validates the whole file in one pass and returns the summary. `<settings>` from the file is only compared with the job settings (the single source of truth) and differences are logged as `info`.
validateBundles()  : array<int, ImportBundleVerdict>
Validates `<bundles>`. Not batched like cards (there are typically far fewer bundles than products and they are available at once: {@see ImportDto::getBundles()} returns a `list`, not a generator), so they are validated in one pass called after the last card: (1) one batch query for existing codes, (2) scalar and structure check plus registering codes into `$context->seenCodes` (shared with variants, `ProductSet.code` is unique across types), (3) composition items (a second pass like accessories, but a hard error: a bundle without items makes no sense). An unknown code therefore also blocks a bundle whose scalar part was fine.
validateChunk()  : array<int, ImportProductVerdict>
Validates one batch of cards: matching with one query ({@see ImportMatcher}) plus a verdict per card.
assetExists()  : bool
Asset existence regardless of module (`cms_system_images` is a shared System table).
collectAccessories()  : void
reportSettingsDiff()  : void
The job decides, but a difference from the file must be visible to the admin, who can edit the job and revalidate.
reportStructureIssues()  : bool
resolveExpectedAction()  : string
Resolves the expected action of an item: a per-item `action` overrides the global `mode`.
stringifySetting()  : string
validateAccessoryTargets()  : void
Accessories may reference a code created in the same file, so they are resolved after the last product: what is not in the file must be in the DB.
validateBundleScalar()  : ImportBundleVerdict
validateCategories()  : void
validateCodes()  : bool
validateCollectionMode()  : void
validateCreateRequirements()  : bool
Required fields on create: code and name in the default language. Returns `true` when the item failed because of them.
validateImages()  : bool
validateLang()  : void
An unknown language alias is a soft error: the rest of the item is written without that text.
validateParameters()  : bool
An unknown parameter or option is a soft error (the code list is match-only unless `autoCreateOptions`). A type mismatch (the data shape does not fit the `ParameterType` of the key: `option`/text on a range type, `value`/`value2` outside a range type) is a hard item error, same as in the writer ({@see ImportProductWriter::buildParameterRelRow()}). The classification lives in {@see ImportRules::classifyParameterKind()} so the rule is not duplicated.
validatePriceList()  : bool
The same price gate for a variant and for a bundle.
validatePrices()  : bool
Prices. Hard item errors: both tags of the `amount`/`amountWithVat` pair (cannot decide which applies), a non-numeric or negative value (a strict `sql_mode` fails the write, a non-strict one silently stores zero or clips it and sells at a wrong price) and `fromAmount` outside an integer >= 0 (the price would land on another axis of the series than the file meant). Soft errors (the price is skipped, the rest of the item is written): unknown currency and `<price>` without a price.
validateProduct()  : ImportProductVerdict
validateProductFields()  : bool
validateProductImage()  : bool
Card image `<image src>` (the schema type `productImageType` has only `src`, no `assetId`).
validateProductTextLanguages()  : bool
Validates card texts per language. Returns `true` when the card failed because of them: - an empty `<name/>` in the default language is a hard error (not null), - an empty `<name lang="x"/>` in a language where the card text exists is a hard error, - a language with texts (`description`) but without a non-empty `<name lang>` where the row does not exist (new card, or card without text in that language) is a soft error: the row is not created, because an empty name would override the language fallback on the front end.
validateScalarValue()  : bool
validateStock()  : bool
Stock. Hard item errors: a non-numeric quantity, a negative one with `mode="set"` (a physical state cannot be negative) and a non-numeric or negative `minQuantity`. Soft errors (stock is skipped, the rest of the item is written): unknown warehouse, ambiguous default warehouse, unknown `mode`.
validateTags()  : void
validateTexts()  : bool
Returns `true` when the item is blocked because of texts (cleared or overlong name).
validateVariant()  : ImportItemVerdict
validateVariantScalars()  : bool
validateVariantTextLanguages()  : void
A language with texts (`longName`/`shortDescription`/`description`) but without a non-empty `<name lang>` where the set translation does not exist (new set, or existing one without text in that language): the row is not created, because an empty name would override the language fallback on the front end. Soft error (`importIncomplete`), the rest of the item is written.

Constants

LOOKUP_CHUNK

Cards per matching query, see {@see ImportMatcher::LOOKUP_CHUNK}.

private int LOOKUP_CHUNK = \App\UI\Api\Eshop\Models\Import\ImportMatcher::LOOKUP_CHUNK

Properties

Methods

findOverlongPathPart()

Returns the first segment of a path like `Elektro / Apple` that does not fit varchar(255). Shared with the writer so that the dry run promises what the write does.

public static findOverlongPathPart(string $path) : string|null
Parameters
$path : string
Return values
string|null

finish()

Ends a run: root issues (reserved tags, CSV encoding), accessory targets and the rewrite of log row counts into the summary.

public finish(ImportDto $dto, ImportValidationContext $context) : ImportValidationSummary

IMPORTANT: bundles (validateBundles()) are called separately by the caller before finish(), not from here; otherwise they would be validated twice (see validate() and ImportExecutor::execute()).

Parameters
$dto : ImportDto
$context : ImportValidationContext
Return values
ImportValidationSummary

validate()

Validates the whole file in one pass and returns the summary. `<settings>` from the file is only compared with the job settings (the single source of truth) and differences are logged as `info`.

public validate(ImportDto $dto, ImportSettingsDto $jobSettings, ImportLogWriter $log) : ImportValidationSummary

Hard errors:

  • matching: unknown or existing code, ambiguous matchBy=ean, duplicate code in the file
  • one card whose codes belong to different Products
  • code owned by a set with another EAN, or <variant id> combined with the code of another set
  • target that is a bundle or a set without a card
  • create on an existing code, update on a missing one (skipped)
  • code, ean/isbn, name or producer over its length; weight or warranty out of range
  • create without code, default-language name or variant; clearing a not-null column
  • both tags of a price pair; non-numeric, negative or overflowing amount
  • points/fromAmount outside the int unsigned range (variants and bundles alike)
  • non-numeric stock quantity, negative target with mode="set", bad minQuantity
  • unknown or reserved tag or attribute; more than 20 images per variant
  • image src scheme other than http(s); parameter type mismatch

Soft errors (logged with a tentative value):

  • missing <name lang> where the text row does not exist
  • unknown currency, <price> without a price
  • category path segment over 255
  • unknown or ambiguous warehouse, unknown stock mode
  • unknown assetId or code list entry
Parameters
$dto : ImportDto
$jobSettings : ImportSettingsDto

job settings

$log : ImportLogWriter
Return values
ImportValidationSummary

validateBundles()

Validates `<bundles>`. Not batched like cards (there are typically far fewer bundles than products and they are available at once: {@see ImportDto::getBundles()} returns a `list`, not a generator), so they are validated in one pass called after the last card: (1) one batch query for existing codes, (2) scalar and structure check plus registering codes into `$context->seenCodes` (shared with variants, `ProductSet.code` is unique across types), (3) composition items (a second pass like accessories, but a hard error: a bundle without items makes no sense). An unknown code therefore also blocks a bundle whose scalar part was fine.

public validateBundles(ImportDto $dto, ImportValidationContext $context) : array<int, ImportBundleVerdict>
Parameters
$dto : ImportDto
$context : ImportValidationContext
Return values
array<int, ImportBundleVerdict> —

in the same order as $dto->getBundles()

assetExists()

Asset existence regardless of module (`cms_system_images` is a shared System table).

private assetExists(int $assetId) : bool
Parameters
$assetId : int
Return values
bool

resolveExpectedAction()

Resolves the expected action of an item: a per-item `action` overrides the global `mode`.

private resolveExpectedAction(string $action, bool $exists, ImportVariantDto $variant, string|null $code, ImportValidationContext $context, bool &$blocked) : string

Returns create/update/skip; a disallowed combination logs a hard error.

Parameters
$action : string
$exists : bool
$variant : ImportVariantDto
$code : string|null
$context : ImportValidationContext
$blocked : bool
Return values
string

stringifySetting()

private stringifySetting(string|bool $value) : string
Parameters
$value : string|bool
Return values
string

validateBundleScalar()

private validateBundleScalar(ImportBundleDto $bundle, array<string, array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}>> $matchesByCode, array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}> $matchesById, ImportValidationContext $context) : ImportBundleVerdict
Parameters
$bundle : ImportBundleDto
$matchesByCode : array<string, array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}>>
$matchesById : array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}>
$context : ImportValidationContext
Return values
ImportBundleVerdict

validateCodes()

private validateCodes(ImportVariantDto $variant, string|null $code, array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}> $matches, ImportValidationContext $context) : bool
Parameters
$variant : ImportVariantDto
$code : string|null
$matches : array<int, array{setId: int, productId: int|null, variantId: int|null, deleted: bool, code: string, ean: string|null, typeId: int, languageIds: list}>
$context : ImportValidationContext
Return values
bool

validateParameters()

An unknown parameter or option is a soft error (the code list is match-only unless `autoCreateOptions`). A type mismatch (the data shape does not fit the `ParameterType` of the key: `option`/text on a range type, `value`/`value2` outside a range type) is a hard item error, same as in the writer ({@see ImportProductWriter::buildParameterRelRow()}). The classification lives in {@see ImportRules::classifyParameterKind()} so the rule is not duplicated.

private validateParameters(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
$variant : ImportVariantDto
$code : string|null
$context : ImportValidationContext
Return values
bool

validatePriceList()

The same price gate for a variant and for a bundle.

private validatePriceList(array<int, ImportPriceDto> $prices, string|null $code, string|null $action, ImportValidationContext $context) : bool

IMPORTANT: bundles are written through the same path (ImportBundleWriter to ImportProductWriter::applyPrices()), so they need the full gate, not only the exclusivity of the amount/amountWithVat pair. Without it abc and 1e1000 passed (the writer turned both into 0.0000, i.e. a free bundle), as did a negative price and an amount too large for the column.

$action exists only for the log: a bundle logs with its action, a variant without one.

Parameters
$prices : array<int, ImportPriceDto>
$code : string|null
$action : string|null
$context : ImportValidationContext
Return values
bool

validatePrices()

Prices. Hard item errors: both tags of the `amount`/`amountWithVat` pair (cannot decide which applies), a non-numeric or negative value (a strict `sql_mode` fails the write, a non-strict one silently stores zero or clips it and sells at a wrong price) and `fromAmount` outside an integer >= 0 (the price would land on another axis of the series than the file meant). Soft errors (the price is skipped, the rest of the item is written): unknown currency and `<price>` without a price.

private validatePrices(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
$variant : ImportVariantDto
$code : string|null
$context : ImportValidationContext
Return values
bool

validateProductImage()

Card image `<image src>` (the schema type `productImageType` has only `src`, no `assetId`).

private validateProductImage(ImportProductDto $product, ImportValidationContext $context) : bool

An empty tag (isNullOut()) clears the image; this is tolerated although the schema marks src required: the schema is aspirational and the PHP validator is the source of truth. A non-empty value must be http(s) (SSRF).

Parameters
$product : ImportProductDto
$context : ImportValidationContext
Return values
bool

validateProductTextLanguages()

Validates card texts per language. Returns `true` when the card failed because of them: - an empty `<name/>` in the default language is a hard error (not null), - an empty `<name lang="x"/>` in a language where the card text exists is a hard error, - a language with texts (`description`) but without a non-empty `<name lang>` where the row does not exist (new card, or card without text in that language) is a soft error: the row is not created, because an empty name would override the language fallback on the front end.

private validateProductTextLanguages(ImportProductDto $product, int|null $targetProductId, ImportValidationContext $context) : bool

The default language of a new card falls back to the first variant (ImportProductWriter), so it is not warned about.

Parameters
$product : ImportProductDto
$targetProductId : int|null
$context : ImportValidationContext
Return values
bool

validateStock()

Stock. Hard item errors: a non-numeric quantity, a negative one with `mode="set"` (a physical state cannot be negative) and a non-numeric or negative `minQuantity`. Soft errors (stock is skipped, the rest of the item is written): unknown warehouse, ambiguous default warehouse, unknown `mode`.

private validateStock(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool

NOTE: a negative result with mode="add" cannot be detected by the dry run (it depends on the live stock); ImportProductWriter::applyStock() checks it at write time.

Parameters
$variant : ImportVariantDto
$code : string|null
$context : ImportValidationContext
Return values
bool

validateVariantTextLanguages()

A language with texts (`longName`/`shortDescription`/`description`) but without a non-empty `<name lang>` where the set translation does not exist (new set, or existing one without text in that language): the row is not created, because an empty name would override the language fallback on the front end. Soft error (`importIncomplete`), the rest of the item is written.

private validateVariantTextLanguages(ImportVariantDto $variant, string|null $code, array<int, int> $existingLanguageIds, ImportValidationContext $context) : void

The default language on create is a hard check in validateCreateRequirements().

Parameters
$variant : ImportVariantDto
$code : string|null
$existingLanguageIds : array<int, int>

languages of the target set texts (empty on create)

$context : ImportValidationContext
On this page

Search results