CoolCMS API

ProductImportManager extends Manager
in package

Manages product import jobs: CRUD plus state transitions kept outside the generic save.

  • setState() is the only channel that may change state. A generic save() resets state and counters to the DB values on UPDATE (see beforeSave()), so a stray state in an edit form payload can never overwrite a running or finished job.
  • beforeDelete() refuses to delete a running job.
  • completedDelete() deletes the import file and the row log (best-effort, after commit).

validate() is the dry run; enqueue() moves validated to queued; run() moves queued to running to done or failed (via ImportExecutor); getStatus() serves progress polling. All work by job id, so cron and the admin buttons share the code.

Tags
extends

Table of Contents

Constants

RUN_NOW_MAX_ITEMS  : int = 200
Maximum number of items for "run now" (synchronous in the admin request).
STALE_RUNNING_SECONDS  : int = 1800
Seconds after `startedTime` when a `running` job is considered dead (`php-fpm` crash, time limit, process kill: `finally` did not run and the job would stay `running` forever).
DELETED_IDS_META  : string = '__deletedIds'
Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
ALLOWED_TRANSITIONS  : array<string|int, mixed> = [ // `validated`/`invalid` to `validated`/`inva...
CHART_BUCKETS  : array<string|int, mixed> = ['day' => '%Y-%m-%d', 'week' => '%x-%v', 'month...
MySQL `DATE_FORMAT` masks for the runs-over-time chart ({@see getRunsChart()}).
EXECUTE_LOG_SUFFIX  : string = '.execute.jsonl'
Suffix of the row log of the real run, a separate file next to the validation log (`phase:"execute"`).
EXECUTE_SUMMARY_SUFFIX  : string = '.execute.summary.json'
Suffix of the real run summary (write counts) next to the execution log.
LOG_SUFFIX  : string = '.validate.jsonl'
Suffix of the validation row log (JSONL) next to the import file.
NOTIFICATION_TYPE_KEY  : string = 'ESHOP_IMPORT_DONE'
Key of the "product import finished" notification type (`cms_system_notification_types.key`).
STATISTICS_DEFAULT_RANGE_DAYS  : int = 30
Default period length in days for {@see getStatistics()} and {@see getRunsChart()} when the caller sends no range.
STATISTICS_DEFAULT_RECENT_LIMIT  : int = 20
Number of latest runs {@see getStatistics()} returns for the admin table.
STATUS_TAIL  : int = 20
Number of last errors/warnings from the execution log returned by `status` polling.
SUMMARY_SUFFIX  : string = '.validate.summary.json'
Suffix of the dry-run summary (predicted create/update/skip counts) next to the log.

Properties

$service  : Service<string|int, TEntity>
Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
$appContext  : AppContext
$assetFormatRegistry  : AssetFormatRegistry|null
Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.
$cacheTagInvalidator  : CacheTagInvalidator|null
Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
$internalCallContext  : InternalCallContext|null
Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).
$languageContext  : LanguageContext|null
$nonInvalidatingColumns  : array<string|int, string>
Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.
$paramsConfig  : Config|null
Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.
$presenterContext  : PresenterContext
$settingContext  : SettingContext
$totalCount  : int|null
$useTransactionForDelete  : bool
$useTransactionForSave  : bool
$em  : EntityManagerInterface
$importExecutor  : ImportExecutor
$importLogReader  : ImportLogReader
$importParserFactory  : ImportParserFactory
$importValidator  : ImportValidator
$notificationManager  : NotificationManager
$notificationTypeManager  : NotificationTypeManager
$productSetManager  : ProductSetManager
$productXmlExporter  : ProductXmlExporter
$rejectedUploads  : array<int, array{name: string, reason: string}>
New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.

Methods

__construct()  : mixed
delete()  : Result
Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:
deleteAll()  : Result
enqueue()  : Result
Queues the job (`validated` to `queued`); the cron `/cron/eshop/product-import/run` picks it up within a minute.
export()  : Result
Exports the catalog in the import format.
failStaleRunning()  : array<int, int>
Marks stuck `running` jobs as `failed` and appends a line to the execution log.
find()  : T|null
findAll()  : T> : list>)
findBy()  : T|null
findRelatedIds()  : array<int, int>|null
Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).
getAppContext()  : AppContext
getDefaultSkipFields()  : array<string|int, string>
Returns keys that the generic `service->save()` must not copy onto the managed entity.
getEntityClass()  : TEntity>
Returns the root Doctrine entity class managed by this manager.
getExternalMetaAllowlist()  : array<string|int, string>
Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).
getExternalOwnerData()  : array<string, mixed>
Returns `data` keys that fill the owner from the server when an external save creates a record.
getExternalRemoteIpData()  : array<string, mixed>
Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
getExternalUpdateColumnsAllowlist()  : array<string|int, string>
Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
getGraphDataKeys()  : array<string|int, string>
Returns the keys an external client may send nested in `data`.
getLanguageContext()  : LanguageContext|null
getPresenterContext()  : PresenterContext
getRunsChart()  : Result
Returns the runs-over-time chart for {@see \App\UI\Admin\Eshop\Components\StatisticsChartComponent\StatisticsChartControl}.
getServiceClass()  : class-string
Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).
getSettingContext()  : SettingContext
getSettings()  : array<int, array<string, mixed>>
getStatistics()  : Result
Returns a summary of runs in `[from, to]` (by `insertTime`): job counts by state, sums of the item counters and the last `$recentLimit` runs for the admin table.
getStatus()  : Result
Returns the job state for `status` polling: counters, state and the last errors/warnings from the execution log. The log path always comes from the job.
getTotalCount()  : int|null
getValidationReport()  : Result
Returns a paginated preview of the row log plus the summary.
initializeAllContexts()  : void
registerEvent()  : Result
run()  : Result
Performs the real run: `queued` to `running` to `done` or `failed`.
runNow()  : Result
Runs the job synchronously from the admin ("run now", up to {@see RUN_NOW_MAX_ITEMS} items).
save()  : Result
setAppContext()  : $this
setAssetFormatRegistry()  : static
setCacheTagInvalidator()  : static
setInternalCallContext()  : static
setLanguageContext()  : $this
setParamsConfig()  : static
setPresenterContext()  : $this
setSettingContext()  : $this
setState()  : Result
Moves the job to a new state with validation; this is the external channel (Api/admin).
updateAllRelations()  : Result
updateColumn()  : Result
updateColumns()  : Result
updateRelations()  : Result
validate()  : Result
Validates the job as a dry run: parses the file, checks matching, code lists and limits, and writes a JSONL row log. Writes nothing to the catalog.
beforeDelete()  : Result|null
Refuses to delete a running job; any other job can be deleted.
beforeSave()  : Result|null
On insert, enforces server defaults (state `uploaded`, zeroed counters) regardless of the form.
beginRejectedUploads()  : array<int, array{name: string, reason: string}>
Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
bumpCacheTags()  : void
Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
bumpCacheTagsExplicit()  : void
Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
bumpCacheTagsForClass()  : void
Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
classCacheTags()  : array<string|int, string>
Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.
completedDelete()  : void
Best-effort file cleanup after the record is deleted (import file and row log).
finishRejectedUploads()  : void
Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.
getDeletedId()  : int|null
Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).
getService()  : Service<string|int, TEntity>
Returns the service backing the manager traits.
readUploadedItem()  : string|null
Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.
recordRejectedUpload()  : void
Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).
appendExecuteLogRow()  : void
Appends an `error` row to the end of the execution log (the run log must not be overwritten). Best-effort.
bumpDeleteCacheTags()  : void
Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
captureDeletedIds()  : WeakMap<object, int>
Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.
columnsInvalidateCache()  : bool
deleteBatch()  : Result
Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.
deleteImportFiles()  : void
deleteImportNotifications()  : void
Deletes the "import finished" notifications of deleted imports, because they link to the import detail and the bell would lead to a 404.
describeAbortProgress()  : string
Returns a suffix for the crash error describing what was written until then (it is not rolled back).
fetchStateRow()  : DateTime}|null
findNotificationTypeId()  : int|null
Returns the notification type id; creating a notification respects the type switch (`$onlyActive`).
invalidFilePathResult()  : Result
isAssetPathAllowed()  : bool
Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
isStale()  : bool
Returns whether a run without `startedTime` or older than {@see STALE_RUNNING_SECONDS} is stuck.
loadEntitiesForCacheTags()  : array<int, object>
Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.
readExecutionSummaryFile()  : array<string, int|bool>
readSummaryFile()  : array<string, int>
resolveExecutionSummaryPath()  : string
resolvePrivateDir()  : string|null
resolveStatisticsRange()  : DateTime}
resolveSummaryPath()  : string
Returns the dry-run summary path: next to the log, same path with another suffix (deleted with it).
rollbackBatch()  : void
saveWithHooks()  : Result
Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
sendDoneNotification()  : void
Sends the admin bell notification of type `ESHOP_IMPORT_DONE` to the user who uploaded the job.
staleCutoff()  : string
Returns the stuck-run boundary as a column value (`started_time < cutoff`), same time basis as {@see isStale()}.
toDateTimeOrNull()  : DateTime|null
toInt()  : int
Casts a `mixed` DBAL value to int (a direct cast of `mixed` fails PHPStan level 9).
toStringValue()  : string
transition()  : Result
Performs an atomic state transition: `UPDATE ... SET state = :new WHERE id = :id AND state = :old`.
updateValidationCounters()  : void
Writes the validation counters outside the generic `save()`, which resets them from the DB on UPDATE.
usesAssetPathTrait()  : bool
writeExecutionSummaryFile()  : void
writeProgress()  : bool
Writes the progress of the real run with raw DBAL on the own table.
writeSummaryFile()  : void

Constants

RUN_NOW_MAX_ITEMS

Maximum number of items for "run now" (synchronous in the admin request).

public int RUN_NOW_MAX_ITEMS = 200

STALE_RUNNING_SECONDS

Seconds after `startedTime` when a `running` job is considered dead (`php-fpm` crash, time limit, process kill: `finally` did not run and the job would stay `running` forever).

public int STALE_RUNNING_SECONDS = 1800

Twice the cron lock TTL (900 s). Cron (failStaleRunning()) marks it failed, and the admin may do the same by hand (setState() guard). A younger run cannot be killed from outside, because it may still be alive.

DELETED_IDS_META

Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.

protected string DELETED_IDS_META = '__deletedIds'

Read it only through getDeletedId().

ALLOWED_TRANSITIONS

private array<string|int, mixed> ALLOWED_TRANSITIONS = [ // `validated`/`invalid` to `validated`/`invalid` is re-validation after editing the job // settings. Without these edges the admin would have to delete the job and upload the // file again just to change `matchBy` or `defaultLang`. 'uploaded' => ['validated', 'invalid', 'cancelled'], 'validated' => ['queued', 'cancelled', 'validated', 'invalid'], 'invalid' => ['validated', 'invalid', 'cancelled'], 'queued' => ['running', 'cancelled'], // `running` to `failed` is also allowed by hand from the admin (stuck run only, guard in // {@see setState()}) and by cron ({@see failStaleRunning()}). `failed` to // `validated`/`invalid` is re-validation of a failed job, e.g. one from another host // without the file. 'running' => ['done', 'failed'], 'failed' => ['validated', 'invalid'], ]

allowed target states per source state (see ProductImportState)

CHART_BUCKETS

MySQL `DATE_FORMAT` masks for the runs-over-time chart ({@see getRunsChart()}).

private array<string|int, mixed> CHART_BUCKETS = ['day' => '%Y-%m-%d', 'week' => '%x-%v', 'month' => '%Y-%m', 'year' => '%Y']

A deliberate copy of OrderStatisticsService::BUCKETS: imports and order revenue are unrelated, so they must not share one class.

EXECUTE_LOG_SUFFIX

Suffix of the row log of the real run, a separate file next to the validation log (`phase:"execute"`).

private string EXECUTE_LOG_SUFFIX = '.execute.jsonl'

EXECUTE_SUMMARY_SUFFIX

Suffix of the real run summary (write counts) next to the execution log.

private string EXECUTE_SUMMARY_SUFFIX = '.execute.summary.json'

LOG_SUFFIX

Suffix of the validation row log (JSONL) next to the import file.

private string LOG_SUFFIX = '.validate.jsonl'

NOTIFICATION_TYPE_KEY

Key of the "product import finished" notification type (`cms_system_notification_types.key`).

private string NOTIFICATION_TYPE_KEY = 'ESHOP_IMPORT_DONE'

STATISTICS_DEFAULT_RANGE_DAYS

Default period length in days for {@see getStatistics()} and {@see getRunsChart()} when the caller sends no range.

private int STATISTICS_DEFAULT_RANGE_DAYS = 30

STATISTICS_DEFAULT_RECENT_LIMIT

Number of latest runs {@see getStatistics()} returns for the admin table.

private int STATISTICS_DEFAULT_RECENT_LIMIT = 20

STATUS_TAIL

Number of last errors/warnings from the execution log returned by `status` polling.

private int STATUS_TAIL = 20

SUMMARY_SUFFIX

Suffix of the dry-run summary (predicted create/update/skip counts) next to the log.

private string SUMMARY_SUFFIX = '.validate.summary.json'

Properties

$service

Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).

public Service<string|int, TEntity> $service

$assetFormatRegistry

Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.

protected AssetFormatRegistry|null $assetFormatRegistry = null

Set by a decorator (decorators.neon) like setParamsConfig(): more than ten managers process assets and each has its own constructor, so constructor injection would touch all of them.

$cacheTagInvalidator

Post-commit invalidator of the wire-payload query cache (tag bump per entity class).

protected CacheTagInvalidator|null $cacheTagInvalidator = null

Set by the decorator (decorators.neon) like setParamsConfig(). Nullable and fail-open: without wiring (or without Redis) a bump is a no-op.

$internalCallContext

Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).

protected InternalCallContext|null $internalCallContext = null

Only ApiBridge::internalCall() sets it, never the payload and not the RBAC guard, so the emergency apiRbac.internalMode: off cannot disable it. Set by a decorator like setAssetFormatRegistry(). Without it (direct manager call from cron or a test) the call counts as external.

$nonInvalidatingColumns

Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.

protected array<string|int, string> $nonInvalidatingColumns = ['views', 'viewsList', 'viewsDetail']

Covers all view counters across modules: views (Blog Article), viewsList (list impression of Advert/Message) and viewsDetail (detail view of Advert/Message, registerEvent() in the *DetailTraits). IMPORTANT: viewsDetail must stay in the list, otherwise every open of an advert/post detail would invalidate all lists of the class.

$paramsConfig

Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.

protected Config|null $paramsConfig = null

Write logic (asset directories) reads paths from here, not from getSettings() or the setting context, which may be filled by the client without server paths. The property cannot be named $config: it collides with the promoted $config in some managers.

$useTransactionForDelete

protected bool $useTransactionForDelete = true

$useTransactionForSave

protected bool $useTransactionForSave = true

$rejectedUploads

New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.

private array<int, array{name: string, reason: string}> $rejectedUploads = []

Methods

__construct()

public __construct(ProductImportService $service, EntityManagerInterface $em, ImportParserFactory $importParserFactory, ImportValidator $importValidator, ImportLogReader $importLogReader, ImportExecutor $importExecutor, ProductSetManager $productSetManager, NotificationManager $notificationManager, NotificationTypeManager $notificationTypeManager, ProductXmlExporter $productXmlExporter) : mixed
Parameters
$service : ProductImportService
$em : EntityManagerInterface
$importParserFactory : ImportParserFactory
$importValidator : ImportValidator
$importLogReader : ImportLogReader
$importExecutor : ImportExecutor
$productSetManager : ProductSetManager
$notificationManager : NotificationManager
$notificationTypeManager : NotificationTypeManager
$productXmlExporter : ProductXmlExporter

delete()

Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:

public delete(int|array<string|int, mixed> $id[, array<string, mixed> $meta = [] ]) : Result
  • beforeDelete(array $entities, array $meta): ?Result runs before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows into afterDelete under $meta['__beforeDelete'].
  • afterDelete(array $entities, Result $result, array $meta): ?Result runs after the delete, still in the transaction. It is only for recomputing derived state from the remaining rows (toped_until, member_until, county), idempotent and reversible. A failure rolls back the delete itself (atomicity). It is also the place for the mandatory delete of dependents through a foreign manager (comment, rating and message threads): an owner still managed in the unit of work references them, so they must go after its row, never before it in beforeDelete (collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only into completedDelete.
  • completedDelete(array $entities, Result $result, array $meta): void runs best effort after the commit, outside the transaction; an exception is only logged (the delete stays).
  • completedAssetCleanup(array $entities, Result $result, array $meta): void runs best effort after the commit and releases the Asset rows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in is ownedAssetIds(object $entity): array on the manager, without it the hook does not run.

IMPORTANT: the cleanup has its own name instead of completedDelete() because many Api managers define completedDelete() themselves and a class method overrides a trait method without a warning, so the cleanup would silently be missing in them.

IMPORTANT: a soft delete (deleted = 1) goes through save(), not here; assets must not be released there because the record can be restored.

$entities are the entities loaded before the delete (after the DELETE the row is gone, but the PHP object keeps the data, such as FKs for the recomputation).

IMPORTANT: the id is the exception. Doctrine nulls the id of a removed instance on flush(), so a hook must not rely on getId() once the delete ran (today only the read UoW::clear() in the service find() hands the preloaded entities over detached and keeps their id). Every hook therefore gets the ids captured before beforeDelete under the reserved key DELETED_IDS_META, keyed by the loaded entity object (an input id that loaded nothing has no entry); read it through getDeletedId(). The key is always set here, so a caller-supplied value never survives.

Without hooks it behaves exactly as before (no preloading, no transaction). NOTE: deleteAll() (bulk) and the tree deleteNode take another path and do not trigger these hooks.

Parameters
$id : int|array<string|int, mixed>
$meta : array<string, mixed> = []
Return values
Result

deleteAll()

public deleteAll([array<string|int, mixed> $filters = [] ]) : Result
Parameters
$filters : array<string|int, mixed> = []
Return values
Result

enqueue()

Queues the job (`validated` to `queued`); the cron `/cron/eshop/product-import/run` picks it up within a minute.

public enqueue(int $id) : Result

The job must be validated.

Parameters
$id : int
Return values
Result

export()

Exports the catalog in the import format.

public export(array<int, int> $productIds, array<int, int> $productSetIds, array<int, string> $languages, bool $includeIds, array<int, string> $sections) : Result

Creates no job and no DB row; it only reads and writes a file into privateDir. The guard is getAll (admin bridge presenter), because this reads the catalog and does not write it.

Parameters
$productIds : array<int, int>
$productSetIds : array<int, int>
$languages : array<int, string>
$includeIds : bool
$sections : array<int, string>
Return values
Result

failStaleRunning()

Marks stuck `running` jobs as `failed` and appends a line to the execution log.

public failStaleRunning() : array<int, int>

Called by cron before picking from the queue when nobody holds the run lock. Affects every running job with startedTime older than STALE_RUNNING_SECONDS. Without this, a job would stay running forever after a php-fpm crash: it could not be deleted (beforeDelete()), the queue would ignore it and the admin would have no button.

Return values
array<int, int> —

ids of the marked jobs

find()

public find(int $id[, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
$id : int
$includes : string|array<string|int, mixed> = []
Return values
T|null

findAll()

public findAll([array<string|int, mixed>|null $includes = [] ][, array<string|int, mixed>|null $filters = [] ][, array<string|int, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ]) : T> : list>)
Parameters
$includes : array<string|int, mixed>|null = []
$filters : array<string|int, mixed>|null = []
$orderBys : array<string|int, mixed>|null = null
$offset : int|null = null
$limit : int|null = null
$columns : array<string|int, mixed>|null = null
$computeTotalCount : bool = false
Return values
T> : list>) —

entities, or raw rows when $columns is set

findBy()

public findBy([array<string|int, mixed>|null $filters = [] ][, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
$filters : array<string|int, mixed>|null = []
$includes : string|array<string|int, mixed> = []
Return values
T|null

findRelatedIds()

Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).

public findRelatedIds(int $parentId, string $property, string $targetAssociation) : array<int, int>|null
Parameters
$parentId : int
$property : string
$targetAssociation : string
Return values
array<int, int>|null —

null when it cannot be determined (collection mapping missing / database error)

getDefaultSkipFields()

Returns keys that the generic `service->save()` must not copy onto the managed entity.

public getDefaultSkipFields() : array<string|int, string>

The manager's own afterSave()/beforeSave() sync trait handles them through $meta, not through the object graph. Backed by an optional protected per-manager property. The write Api endpoint (SaveTrait::internalActionSave()) also uses this accessor to strip the same keys from $data before hydration, because defaultSkipFields alone does not protect hydration.

Return values
array<string|int, string>

getEntityClass()

Returns the root Doctrine entity class managed by this manager.

public getEntityClass() : TEntity>

The write Api endpoint (SaveTrait) takes it from here instead of the request, so a client cannot choose the instantiated PHP class.

Return values
TEntity>

getExternalMetaAllowlist()

Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).

public getExternalMetaAllowlist() : array<string|int, string>

The internal path (ApiBridge::internalCall() calling internalActionSave() directly, admin and front) is not limited by this allowlist and reads $meta unchanged. The default is an empty array, so external meta is dropped entirely; actionSave() is reachable through HTTP routing regardless of ApiBridge, hence the strip. Override per manager with the protected property $externalMetaAllowlist, analogous to self::getDefaultSkipFields().

Return values
array<string|int, string>

getExternalOwnerData()

Returns `data` keys that fill the owner from the server when an external save creates a record.

public getExternalOwnerData(int $userId) : array<string, mixed>

The default [] means the domain has no owner or must not be created through external save (Order uses the dedicated checkout action, User registers another way). On INSERT there is nothing to own, so the owner cannot be verified, only determined: the owner field is #[ExternalReadonly] (the strip removes it from the payload) and SaveTrait::applyExternalOwner() then fills it from the JWT, so the client value never reaches hydration.

IMPORTANT: return the key that matches the column mapping, otherwise the owner is silently not written:

  • Watchdog/Comment/Rating/Message: scalar column, ['userId' => $userId];
  • Advert (Bazaar/Comcat): mapped column, ['insertUserId' => $userId];
  • Bazaar Response/AdvertFollower: the scalar userId has an #[ORM\AttributeOverride] with insertable: false, updatable: false in the Api leaf and the column belongs to the association $user, so ['user' => ['id' => $userId]]; Comcat Response has no $user association, so there it is ['userId' => $userId];
  • Complaint: the owner is the association $user, ['user' => ['id' => $userId]].

['id' => X] is a reference that hydrateForPersist() looks up in the DB (OrmEntityHydrator::isReference()). Called only on the external path and only on INSERT; on UPDATE it would make whoever saves a foreign record its owner.

Parameters
$userId : int

identity from the JWT (never from the request)

Return values
array<string, mixed>

getExternalRemoteIpData()

Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.

public getExternalRemoteIpData(string $ip) : array<string, mixed>

The IP field is #[ExternalReadonly], so the external strip removes it and the client cannot forge it. Without filling, an external insert would have no IP and the "anonymous by IP" deduplication and per-IP manager limits would silently stop working. SaveTrait::applyExternalRemoteIp() fills it from IRequest::getRemoteAddress(), never from the request. The default [] means the entity carries no IP; overrides return the entity field name (['insertIp' => $ip]).

Parameters
$ip : string
Return values
array<string, mixed>

getExternalUpdateColumnsAllowlist()

Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.

public getExternalUpdateColumnsAllowlist() : array<string|int, string>

updateColumn on a to-many relation is DELETE-all plus INSERT, so without this filter an external POST with values={userRoles: [...]} would bypass the strip/guard of the save path (privilege escalation). The internal path (ApiBridge::internalCall() calling internalActionUpdateColumns() directly) is not limited. The default is an empty array, so external updateColumns is rejected entirely. Override with the protected property $externalUpdateColumnsAllowlist.

Return values
array<string|int, string>

getGraphDataKeys()

Returns the keys an external client may send nested in `data`.

public getGraphDataKeys() : array<string|int, string>

Before hydration the write Api endpoint (SaveTrait::internalActionSave()) moves them from $data to $meta, where the manager's own afterSave()/beforeSave() sync trait reads them (the same mechanism as internal meta). When the client also sends the same key explicitly in meta, meta wins (compatibility with admin, e.g. the categoryRels collision on Advert). Backed by an optional protected per-manager property, like self::getDefaultSkipFields().

IMPORTANT: promoting a key here does not make its content safe to write as received. When the key carries a sensitive field that the server otherwise dictates (typically a payment or approval flag), the manager must overwrite or verify it server-side on the external path in beforeSave()/afterSave() before its sync trait reads it; otherwise the client sets it itself (privilege escalation). The external path is recognized by $meta['__external'] === true, which only SaveTrait::actionSave() sets (never internalActionSave() alone), so internal calls never get the marker. Example: Bazaar Advert.categoryRels is persisted through the object graph (owned collection outside $graphDataKeys); a client could mark paid itself and get a paid advert without payment, so AdvertManager::beforeSave() overwrites it through CategoryRelsPaidSanitizationTrait::sanitizeExternalCategoryRelsPaid() before flush.

Return values
array<string|int, string>

getRunsChart()

Returns the runs-over-time chart for {@see \App\UI\Admin\Eshop\Components\StatisticsChartComponent\StatisticsChartControl}.

public getRunsChart([DateTime|null $from = null ][, DateTime|null $to = null ][, string $granularity = 'month' ]) : Result

Counts uploaded jobs per bucket (day, week, month, year) in the ChartJS shape {labels, datasets}. Empty buckets are filled with zero so the chart has no gaps, same as OrderStatisticsService::bucketLabels(); the bucket masks are a separate copy (CHART_BUCKETS).

Parameters
$from : DateTime|null = null
$to : DateTime|null = null
$granularity : string = 'month'
Return values
Result —

data: array{labels: list, datasets: list<array{key: string, label: string, data: list}>}

getServiceClass()

Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).

public getServiceClass() : class-string

The cache is controlled at the Api presenter boundary through this identity.

Return values
class-string

getSettings()

public getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>

getStatistics()

Returns a summary of runs in `[from, to]` (by `insertTime`): job counts by state, sums of the item counters and the last `$recentLimit` runs for the admin table.

public getStatistics([DateTime|null $from = null ][, DateTime|null $to = null ][, int $recentLimit = self::STATISTICS_DEFAULT_RECENT_LIMIT ]) : Result

Read only; aggregated through DBAL because tens or hundreds of rows need no Doctrine hydration.

Parameters
$from : DateTime|null = null
$to : DateTime|null = null
$recentLimit : int = self::STATISTICS_DEFAULT_RECENT_LIMIT
Return values
Result —

data: array{from: string, to: string, byState: array<string, int>, totalJobs: int, createdCount: int, updatedCount: int, skippedCount: int, errorCount: int, warningCount: int, recent: list<array<string, mixed>>}

getStatus()

Returns the job state for `status` polling: counters, state and the last errors/warnings from the execution log. The log path always comes from the job.

public getStatus(int $id) : Result
Parameters
$id : int
Return values
Result —

data: array{state: string, totalItems: int, processedItems: int, createdCount: int, updatedCount: int, skippedCount: int, errorCount: int, warningCount: int, startedTime: ?string, finishedTime: ?string, finished: bool, recentIssues: list<array<string, mixed>>}

getTotalCount()

public getTotalCount() : int|null
Return values
int|null

getValidationReport()

Returns a paginated preview of the row log plus the summary.

public getValidationReport(int $id[, int $page = 1 ][, int $perPage = 50 ][, string|null $level = null ][, string $phase = ImportLogWriter::PHASE_VALIDATE ]) : Result

The log path comes only from the job: the admin sends just id, page and level filter. $phase = 'execute' reads the log and summary of the real run (a separate file next to the validation one); the default validate is the dry-run report.

Parameters
$id : int
$page : int = 1
$perPage : int = 50
$level : string|null = null
$phase : string = ImportLogWriter::PHASE_VALIDATE
Return values
Result —

data: array{summary: array<string, int>, items: list<array<string, mixed>>, total: int, counts: array<string, int>, page: int, perPage: int, hasLog: bool, phase: string}

registerEvent()

public registerEvent(array<string|int, int> $ids, class-string $entityClass, string $property) : Result
Parameters
$ids : array<string|int, int>
$entityClass : class-string

entity used to translate the property into a column in the expression

$property : string
Return values
Result

run()

Performs the real run: `queued` to `running` to `done` or `failed`.

public run(int $id[, callable(): void|null $heartbeat = null ]) : Result

Parses the file with the same parser as validation and hands it to ImportExecutor with an execute phase log. After every batch ($afterBatch of the executor) progress is written to the job with raw DBAL on its own table (the generic save resets counters from the DB, see beforeSave()) and $heartbeat is called to renew the cron lock. After the run: ES reindex of touched sets (ProductSetManager::reindexSearch()), the "import finished" bell notification and the final state.

failed means stopOnError stopped the run or the run crashed as a whole (parse, exception outside an item). Errors of single items do not fail the run: the job ends done even with errorCount > 0.

IMPORTANT: the job is reloaded by id after the executor, because the executor calls managers that clear the EM; a stale entity would be written back.

The run goes over HTTP (cron or "run now" in an admin request): PHP max_execution_time is disabled and a client disconnect does not kill the run. php-fpm/webserver time limits cannot be influenced, so stuck running jobs are caught by failStaleRunning() and "run now" has an item limit. The queued to running transition is atomic (transition()), so two concurrent callers never start the same job.

Parameters
$id : int
$heartbeat : callable(): void|null = null
Return values
Result

runNow()

Runs the job synchronously from the admin ("run now", up to {@see RUN_NOW_MAX_ITEMS} items).

public runNow(int $id) : Result

Moves validated to queued and calls run() in one request. The caller (admin trait) holds the lock against concurrent cron runs.

Parameters
$id : int
Return values
Result

save()

public save(TEntity $entity[, array<string, mixed> $meta = [] ][, array<int, string> $skipFields = [] ]) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed> = []
$skipFields : array<int, string> = []
Return values
Result

setParamsConfig()

public setParamsConfig(Config $paramsConfig) : static
Parameters
$paramsConfig : Config
Return values
static

setState()

Moves the job to a new state with validation; this is the external channel (Api/admin).

public setState(int $id, ProductImportState $state) : Result

A disallowed transition returns VALIDATION_FAILED and writes nothing. running sets startedTime, done/failed set finishedTime.

From outside, running to failed is allowed only for a stuck run (startedTime older than STALE_RUNNING_SECONDS); a live run is never killed from the admin. The run itself (run()) goes through transition(), which has no such guard.

The age is checked again inside the UPDATE (started_time IS NULL OR started_time < cutoff): between the SELECT and the write the job may have gone failed, validated, queued, running, and the new run must not be killed based on the stale read.

Parameters
$id : int
$state : ProductImportState
Return values
Result

updateAllRelations()

public updateAllRelations(int|array<string|int, mixed> $entityId, string $property, array<string, mixed> $values) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$values : array<string, mixed>

relation property name => value set on every row

Return values
Result

updateColumn()

public updateColumn(int|array<string|int, mixed> $entityId, string $property, mixed $value) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$value : mixed
Return values
Result

updateColumns()

public updateColumns(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$values : array<string, mixed>

property name => new value

Return values
Result

updateRelations()

public updateRelations(int|array<string|int, mixed> $entityId, string $property, array<string|int, object> $relationEntities) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$relationEntities : array<string|int, object>

relation entities with updated values

Return values
Result

validate()

Validates the job as a dry run: parses the file, checks matching, code lists and limits, and writes a JSONL row log. Writes nothing to the catalog.

public validate(int $id) : Result

Only the job itself changes: totalItems, errorCount, warningCount, logPath and the state (validated or invalid).

IMPORTANT: counters are written here, not by the generic save(), which resets them to the DB values on UPDATE (beforeSave()). State changes only through setState(). createdCount/updatedCount/skippedCount are deliberately left alone: they count what was really written by the executor, not a forecast. The forecast lives in the summary next to the log (resolveSummaryPath()) and the admin report reads it.

Called synchronously after upload (add) and by the "validate again" button (edit). The method is idempotent and takes only id, so it can be moved to a queue without changing the interface.

Parameters
$id : int
Return values
Result

beforeDelete()

Refuses to delete a running job; any other job can be deleted.

protected beforeDelete(array<int, ProductImport> $entities[, array<string, mixed> $meta = [] ]) : Result|null
Parameters
$entities : array<int, ProductImport>
$meta : array<string, mixed> = []
Return values
Result|null

beforeSave()

On insert, enforces server defaults (state `uploaded`, zeroed counters) regardless of the form.

protected beforeSave(bool $isInsert, ProductImport &$entity[, array<string, mixed> &$meta = [] ]) : Result|null

On update, state, counters and times are never changed by the generic save; they are reset to the current DB values. The only write channels are setState() and the executor.

Parameters
$isInsert : bool
$entity : ProductImport
$meta : array<string, mixed> = []
Return values
Result|null

beginRejectedUploads()

Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.

protected beginRejectedUploads() : array<int, array{name: string, reason: string}>
Return values
array<int, array{name: string, reason: string}> —

the outer save's list

bumpCacheTags()

Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.

protected bumpCacheTags(object $entity) : void

Fail-open (the invalidator swallows exceptions). Called from the manager Save/Delete/Tree traits after the commit.

Parameters
$entity : object

bumpCacheTagsExplicit()

Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).

protected bumpCacheTagsExplicit(array<string|int, string> $classes) : void
Parameters
$classes : array<string|int, string>

bumpCacheTagsForClass()

Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).

protected bumpCacheTagsForClass(class-string $rootClass[, array<string|int, string> $extraTags = [] ]) : void

Bumps the root class and its owned-cascade classes.

Parameters
$rootClass : class-string
$extraTags : array<string|int, string> = []

classCacheTags()

Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.

protected classCacheTags() : array<string|int, string>

IMPORTANT: additionalCacheTags(object) receives an entity, so a class-level bump cannot use it; without this hook a delete would lose the cross-entity tag (Comment to CommentThread). A manager whose cross-entity tag does not depend on a particular entity returns it here.

NOTE: unlike additionalCacheTags(), this hook is declared (with an empty default) instead of being an opt-in through method_exists(); method_exists() is always true in a manager that has the method, which phpstan reports as alreadyNarrowedType.

Return values
array<string|int, string>

completedDelete()

Best-effort file cleanup after the record is deleted (import file and row log).

protected completedDelete(array<int, ProductImport> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void

The job can be deleted even when the file no longer exists on disk.

Parameters
$entities : array<int, ProductImport>

preloaded before the delete

$result : Result
$meta : array<string, mixed> = []

finishRejectedUploads()

Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.

protected finishRejectedUploads(Result|null $result, array<int, array{name: string, reason: string}> $outer) : void
Parameters
$result : Result|null
$outer : array<int, array{name: string, reason: string}>

list from beginRejectedUploads()

getDeletedId()

Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).

protected getDeletedId(array<string, mixed> $meta, object $entity) : int|null
Parameters
$meta : array<string, mixed>
$entity : object
Return values
int|null

getService()

Returns the service backing the manager traits.

protected getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>

readUploadedItem()

Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.

protected readUploadedItem(array<string|int, mixed> $item[, bool $strictBase64 = true ][, int|null $maxBytes = null ]) : string|null

Call it right before processing and release the result before the next item, so that files never meet in memory. A path is accepted only during an internal bridge call ($internalCallContext) and only inside the dropzone root. A rejected path is logged to the auth channel and null is returned; callers treat it like unreadable data (skip the item or refuse the save).

Parameters
$item : array<string|int, mixed>
$strictBase64 : bool = true

false for callers that decode loosely (base64_decode($x))

$maxBytes : int|null = null

size limit; for path it is checked before reading

Return values
string|null

recordRejectedUpload()

Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).

protected recordRejectedUpload(mixed $name, string $reason) : void

Called by the places that drop an item (processOrderedAssets(), advert attachments, product set gallery).

Parameters
$name : mixed
$reason : string

appendExecuteLogRow()

Appends an `error` row to the end of the execution log (the run log must not be overwritten). Best-effort.

private appendExecuteLogRow(string $filePath, string $message) : void
Parameters
$filePath : string
$message : string

bumpDeleteCacheTags()

Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.

private bumpDeleteCacheTags(array<int, object> $entities) : void
Parameters
$entities : array<int, object>

captureDeletedIds()

Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.

private captureDeletedIds(array<int, object> $entities) : WeakMap<object, int>
Parameters
$entities : array<int, object>
Return values
WeakMap<object, int>

columnsInvalidateCache()

private columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
$columns : array<int, string>
Return values
bool

deleteBatch()

Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.

private deleteBatch(array<int, int|string|array<string|int, mixed>> $ids) : Result

Transaction ownership: the wrapper only opens its own level; inside a caller's transaction a rollback here marks the caller's transaction rollback-only, so its final commit rolls the whole scope back (TransactionManager counter contract, the same as the hooked delete() path).

Parameters
$ids : array<int, int|string|array<string|int, mixed>>
Return values
Result

deleteImportNotifications()

Deletes the "import finished" notifications of deleted imports, because they link to the import detail and the bell would lead to a 404.

private deleteImportNotifications(array<int, ProductImport> $entities, array<string, mixed> $meta) : void

Best-effort like the files: a cleanup failure does not fail the import delete.

The import ids come from the delete snapshot in $meta: Doctrine nulls the id of the removed instance on flush.

Parameters
$entities : array<int, ProductImport>
$meta : array<string, mixed>

fetchStateRow()

private fetchStateRow(int $id) : DateTime}|null
Parameters
$id : int
Return values
DateTime}|null

findNotificationTypeId()

Returns the notification type id; creating a notification respects the type switch (`$onlyActive`).

private findNotificationTypeId([bool $onlyActive = true ]) : int|null

Cleanup after an import is deleted ignores a disabled type, because records from the time it was enabled must disappear too.

Parameters
$onlyActive : bool = true
Return values
int|null

isAssetPathAllowed()

Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.

private isAssetPathAllowed(BaseEntity $entity) : bool
Parameters
$entity : BaseEntity
Return values
bool

isStale()

Returns whether a run without `startedTime` or older than {@see STALE_RUNNING_SECONDS} is stuck.

private isStale(DateTime|null $startedTime) : bool
Parameters
$startedTime : DateTime|null
Return values
bool

loadEntitiesForCacheTags()

Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.

private loadEntitiesForCacheTags(int|array<string|int, mixed> $id) : array<int, object>
Parameters
$id : int|array<string|int, mixed>
Return values
array<int, object>

readExecutionSummaryFile()

private readExecutionSummaryFile(string $base, string $logRelativePath) : array<string, int|bool>
Parameters
$base : string
$logRelativePath : string
Return values
array<string, int|bool>

readSummaryFile()

private readSummaryFile(string $base, string $logRelativePath) : array<string, int>
Parameters
$base : string
$logRelativePath : string
Return values
array<string, int>

resolveExecutionSummaryPath()

private resolveExecutionSummaryPath(string $executeLogRelativePath) : string
Parameters
$executeLogRelativePath : string
Return values
string

resolvePrivateDir()

private resolvePrivateDir() : string|null
Return values
string|null

resolveStatisticsRange()

private resolveStatisticsRange(DateTime|null $from, DateTime|null $to) : DateTime}
Parameters
$from : DateTime|null
$to : DateTime|null
Return values
DateTime}

resolveSummaryPath()

Returns the dry-run summary path: next to the log, same path with another suffix (deleted with it).

private resolveSummaryPath(string $logRelativePath) : string
Parameters
$logRelativePath : string
Return values
string

saveWithHooks()

Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.

private saveWithHooks(TEntity $entity, array<string, mixed> $meta, array<int, string> $skipFields) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed>
$skipFields : array<int, string>
Return values
Result

sendDoneNotification()

Sends the admin bell notification of type `ESHOP_IMPORT_DONE` to the user who uploaded the job.

private sendDoneNotification(int $id, int|null $userId, ImportExecutionSummary $summary, bool $failed) : void

itemId is the job id (link to edit). When the type row does not exist, the notification is silently skipped. Best-effort: a failure must not fail the finished run.

Parameters
$id : int
$userId : int|null
$summary : ImportExecutionSummary
$failed : bool

staleCutoff()

Returns the stuck-run boundary as a column value (`started_time < cutoff`), same time basis as {@see isStale()}.

private staleCutoff() : string
Return values
string

toDateTimeOrNull()

private toDateTimeOrNull(mixed $value) : DateTime|null
Parameters
$value : mixed
Return values
DateTime|null

toInt()

Casts a `mixed` DBAL value to int (a direct cast of `mixed` fails PHPStan level 9).

private toInt(mixed $value) : int
Parameters
$value : mixed
Return values
int

toStringValue()

private toStringValue(mixed $value) : string
Parameters
$value : mixed
Return values
string

transition()

Performs an atomic state transition: `UPDATE ... SET state = :new WHERE id = :id AND state = :old`.

private transition(int $id, ProductImportState $state[, string|null $staleCutoff = null ][, string|null $runStartedTime = null ][, string|null &$startedTime = null ]) : Result

Raw DBAL on the own table. Two processes (cron and "run now", or a double click) never run the same job twice or revive a canceled one; the loser gets VALIDATION_FAILED ("state changed meanwhile"). After the write the entity in the identity map is refreshed so the caller does not see the old state.

Optional guards live directly in the UPDATE:

  • $staleCutoff (outside stale paths: setState() to Failed, failStaleRunning()) adds AND (started_time IS NULL OR started_time < ?), so a freshly restarted run is not killed;
  • $runStartedTime (worker run identity, run()) adds AND started_time = ?, so an old worker cannot finish a job that a new run has taken over. Zero affected rows with guards means rejection. $startedTime receives the value written by the transition to running (the run token).
Parameters
$id : int
$state : ProductImportState
$staleCutoff : string|null = null
$runStartedTime : string|null = null
$startedTime : string|null = null
Return values
Result

updateValidationCounters()

Writes the validation counters outside the generic `save()`, which resets them from the DB on UPDATE.

private updateValidationCounters(int $id, ImportValidationSummary $summary, string $logRelativePath) : void

Only values produced by validation change.

IMPORTANT: the job is reloaded by id instead of using the caller's instance. Code lists and matching queries inside validation go through Api managers that clear the EntityManager, so the original instance is detached and flush() on it silently does nothing; logPath would stay empty and the admin report would be permanently empty.

Parameters
$id : int
$summary : ImportValidationSummary
$logRelativePath : string

usesAssetPathTrait()

private static usesAssetPathTrait(string $class) : bool
Parameters
$class : string
Return values
bool

writeProgress()

Writes the progress of the real run with raw DBAL on the own table.

private writeProgress(int $id, ImportExecutionSummary $summary, int $totalItems[, string|null $executeLogPath = null ][, string|null $runStartedTime = null ]) : bool

The generic save() resets counters from the DB on UPDATE (beforeSave()), and a find() between batches would hit a cleared EM. totalItems never drops below the validation value.

With $runStartedTime it writes only to its own run (AND started_time = ?). Returns false only on confirmed loss of ownership (0 rows and a different started_time in the DB; mysqli reports only changed rows, so 0 can also mean identical counters). A DB outage is fail-open (true, logged).

Parameters
$id : int
$summary : ImportExecutionSummary
$totalItems : int
$executeLogPath : string|null = null
$runStartedTime : string|null = null
Return values
bool
On this page

Search results