ImportValidator
in package
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
$codebookLoader read-only
private
ImportCodebookLoader
$codebookLoader
$em read-only
private
EntityManagerInterface
$em
$matcher read-only
private
ImportMatcher
$matcher
Methods
__construct()
public
__construct(ImportCodebookLoader $codebookLoader, ImportMatcher $matcher, EntityManagerInterface $em) : mixed
Parameters
- $codebookLoader : ImportCodebookLoader
- $matcher : ImportMatcher
- $em : EntityManagerInterface
begin()
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()}.
public
begin(ImportDto $dto, ImportSettingsDto $jobSettings, ImportLogWriter $log) : ImportValidationContext
Parameters
- $dto : ImportDto
- $jobSettings : ImportSettingsDto
- $log : ImportLogWriter
Return values
ImportValidationContextfindOverlongPathPart()
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|nullfinish()
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
ImportValidationSummaryvalidate()
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
createon an existing code,updateon a missing one (skipped)- code,
ean/isbn, name or producer over its length;weightorwarrantyout of range - create without
code, default-languagenameor variant; clearing a not-null column - both tags of a price pair; non-numeric, negative or overflowing amount
points/fromAmountoutside the int unsigned range (variants and bundles alike)- non-numeric stock quantity, negative target with
mode="set", badminQuantity - unknown or reserved tag or attribute; more than 20 images per variant
- image
srcscheme other thanhttp(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
assetIdor code list entry
Parameters
- $dto : ImportDto
- $jobSettings : ImportSettingsDto
-
job settings
- $log : ImportLogWriter
Return values
ImportValidationSummaryvalidateBundles()
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()
validateChunk()
Validates one batch of cards: matching with one query ({@see ImportMatcher}) plus a verdict per card.
public
validateChunk(array<int, ImportProductDto> $products, ImportValidationContext $context) : array<int, ImportProductVerdict>
Parameters
- $products : array<int, ImportProductDto>
- $context : ImportValidationContext
Return values
array<int, ImportProductVerdict> —in the same order as $products
assetExists()
Asset existence regardless of module (`cms_system_images` is a shared System table).
private
assetExists(int $assetId) : bool
Parameters
- $assetId : int
Return values
boolcollectAccessories()
private
collectAccessories(ImportVariantDto $variant, ImportValidationContext $context) : void
Parameters
- $variant : ImportVariantDto
- $context : ImportValidationContext
reportSettingsDiff()
The job decides, but a difference from the file must be visible to the admin, who can edit the job and revalidate.
private
reportSettingsDiff(ImportDto $dto, ImportValidationContext $context) : void
Parameters
- $dto : ImportDto
- $context : ImportValidationContext
reportStructureIssues()
private
reportStructureIssues(array<int, ImportStructureIssueDto> $issues, int $fallbackLine, string|null $code, ImportValidationContext $context) : bool
Parameters
- $issues : array<int, ImportStructureIssueDto>
- $fallbackLine : int
- $code : string|null
- $context : ImportValidationContext
Return values
bool —true when the item is blocked because of its shape
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
stringstringifySetting()
private
stringifySetting(string|bool $value) : string
Parameters
- $value : string|bool
Return values
stringvalidateAccessoryTargets()
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.
private
validateAccessoryTargets(ImportValidationContext $context) : void
Parameters
- $context : ImportValidationContext
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
ImportBundleVerdictvalidateCategories()
private
validateCategories(ImportProductDto $product, ImportValidationContext $context) : void
Parameters
- $product : ImportProductDto
- $context : ImportValidationContext
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
boolvalidateCollectionMode()
private
validateCollectionMode(string|null $mode, int $line, string|null $code, ImportValidationContext $context) : void
Parameters
- $mode : string|null
- $line : int
- $code : string|null
- $context : ImportValidationContext
validateCreateRequirements()
Required fields on create: code and name in the default language. Returns `true` when the item failed because of them.
private
validateCreateRequirements(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
- $variant : ImportVariantDto
- $code : string|null
- $context : ImportValidationContext
Return values
boolvalidateImages()
private
validateImages(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
- $variant : ImportVariantDto
- $code : string|null
- $context : ImportValidationContext
Return values
boolvalidateLang()
An unknown language alias is a soft error: the rest of the item is written without that text.
private
validateLang(ImportValue $value, int $line, string|null $code, ImportValidationContext $context) : void
Parameters
- $value : ImportValue
- $line : int
- $code : string|null
- $context : ImportValidationContext
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
boolvalidatePriceList()
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
boolvalidatePrices()
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
boolvalidateProduct()
private
validateProduct(ImportProductDto $product, ImportValidationContext $context) : ImportProductVerdict
Parameters
- $product : ImportProductDto
- $context : ImportValidationContext
Return values
ImportProductVerdictvalidateProductFields()
private
validateProductFields(ImportProductDto $product, ImportValidationContext $context) : bool
Parameters
- $product : ImportProductDto
- $context : ImportValidationContext
Return values
boolvalidateProductImage()
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
boolvalidateProductTextLanguages()
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
boolvalidateScalarValue()
private
validateScalarValue(string $field, ImportValue $value, string|null $code, ImportValidationContext $context) : bool
Parameters
- $field : string
- $value : ImportValue
- $code : string|null
- $context : ImportValidationContext
Return values
boolvalidateStock()
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
boolvalidateTags()
private
validateTags(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : void
Parameters
- $variant : ImportVariantDto
- $code : string|null
- $context : ImportValidationContext
validateTexts()
Returns `true` when the item is blocked because of texts (cleared or overlong name).
private
validateTexts(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
- $variant : ImportVariantDto
- $code : string|null
- $context : ImportValidationContext
Return values
boolvalidateVariant()
private
validateVariant(ImportVariantDto $variant, ImportProductDto $product, bool $cardBlocked, bool $cardIncomplete, ImportValidationContext $context) : ImportItemVerdict
Parameters
- $variant : ImportVariantDto
- $product : ImportProductDto
- $cardBlocked : bool
- $cardIncomplete : bool
- $context : ImportValidationContext
Return values
ImportItemVerdictvalidateVariantScalars()
private
validateVariantScalars(ImportVariantDto $variant, string|null $code, ImportValidationContext $context) : bool
Parameters
- $variant : ImportVariantDto
- $code : string|null
- $context : ImportValidationContext
Return values
boolvalidateVariantTextLanguages()
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