AssetProcessingTrait
Shared logic of asset (image) processing in the manager layer.
The input is an ordered array of items: {type: 'existing', imageId, order} or: {type: 'new', name, data, order} (base64, external client) {type: 'new', name, path, order} (file in the dropzone temp directory, internal forms)
The trait does:
- saves new files (variants by format) as
Assetentities, - builds the relations (existing and new in one batch),
- inserts all relations (delete and insert are atomic via
SaveTrait), - updates the FK to the main asset (
parent.$asset).
Processing of the bytes is not done here; it is delegated by format (raster, video, vector) to AssetFormatProcessor. A new format is added there.
Tags
Table of Contents
Properties
- $lastAssetRejectionCode : string|null
- Why the last {@see saveAssetVariants()} returned `null` ({@see UploadRejection} `REASON_*`); the caller passes it to the report of rejected items. `null` means the last save passed.
Methods
- assetCleanupRoots() : array<int, string>
- Asset roots inside which {@see removeEmptyAssetDirs()} may remove empty directories; never above them.
- assetDir() : string
- Returns the asset directory from the server neon `params` (through Config), not from `getSettings()`: the shared setting context may be filled by a client without server paths (see the in-process ApiBridge).
- assetIdOf() : int|null
- Normalizes the "current image" to an id. Channels with an ORM association return an entity (even a lazy proxy; `getId()` does not initialize it), channels with a scalar FK return an int.
- assetVariantFilePaths() : array<int, string>
- Returns the absolute paths of the files of the asset just written (variants and the original).
- captureOwnedAssetIds() : array<int, int>
- Ids of the images the given entities own ({@see completedAssetCleanup()} requires the same opt-in `ownedAssetIds()`), captured before the rows are deleted. For paths that bypass {@see DeleteTrait::delete()} and its hooks, such as a subtree delete ({@see TreeTrait::deleteNode()}). Fail-soft: an error of one entity is logged and the others are processed.
- completedAssetCleanup() : void
- Post-commit cleanup of the assets of an entity that was just deleted.
- createAssetRel() : object
- Creates the concrete `*AssetRel` entity linked to the parent and the asset.
- deleteAssetFiles() : void
- Deletes the previously scanned files (see {@see listAssetFilesIn()}). Call only after the FK update to the new Asset succeeded.
- dropForeignExistingItems() : array<int, TItem>
- From the `type: existing` items keeps only those whose id is already linked to the record `$parentId` through `$relationProperty` (read from the database). Other types (`new`) stay unchanged. When the links cannot be determined (`null`), no existing item passes: losing the order is better than attaching a foreign file.
- failureOrNull() : Result|null
- A failed Result to propagate, success is null (the `afterSave` hook returns `?Result`).
- getAssetDirAbs() : string
- Absolute prefix of the asset directory. Implement via {@see assetDir()} (reads neon `params` through Config), e.g. `return $this->assetDir(5, 'advertDir', 'abs');`.
- getAssetDirRel() : string
- Relative prefix of the asset directory. Implement via {@see assetDir()}, e.g. `return $this->assetDir(5, 'advertDir', 'rel');`.
- getAssetFormatKinds() : array<int, AssetFormatKind>
- Media kinds allowed in the dropzone gallery of this manager. The default is images and video. Vector is left out on purpose: an end-user svg is needless exposure even when the sanitizer passes it; enable it explicitly if needed.
- getAssetFormatRegistry() : AssetFormatRegistry
- Requires `$this->assetFormatRegistry` set by the decorator ({@see \App\Core\Base\Api\Models\Managers\Manager::setAssetFormatRegistry()}).
- getAssetMaxBytes() : int|null
- Upper size limit of a new gallery file in bytes, `null` = no limit. The default has none; adverts and company profiles return {@see \App\Core\Security\Upload\ImageUploadSizeLimit::bytes()}.
- getAssetSubDir() : string
- Relative subdirectory within the asset dir (typically Y/m/d/parentId/ or Y/m/parentId/).
- getAssetUsageKey() : string|null
- getAssetVariantType() : string
- Key in assetVariants.neon (parameters.assetVariants), e.g. 'bazaar-adverts', 'discussion-messages', 'catalog-firms'.
- getMainAssetColumn() : string
- Name of the property the main (first) gallery image is written to. The default `asset` is an ORM ManyToOne to `Asset` (Message/Advert/Intro). Entities that keep the main image as a flat FK column without an association (Company `Reference::$imageId`) override the hook; otherwise `updateColumn()` would write to a nonexistent property.
- listAssetFilesIn() : array<int, string>
- Lists the files lying in the target directory before a new image is written.
- orderedAssetsFromMeta() : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
- Turns `meta['orderedAssets']` of the request into the list of dropzone items in the shape {@see processOrderedAssets()} reads. Unknown keys and values of a wrong type are dropped; a present but non-text `path` stays as `''` so the file read rejects it the same way instead of silently skipping it.
- orderedFilesFromMeta() : array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>
- Turns `meta['files']` of the request into the list of files for `processFiles()`/`processComplaintFiles()`.
- processOrderedAssets() : Result|null
- Shared template method for processing an ordered array of dropzone assets.
- releaseAsset() : bool
- Releases the old asset after an image was cleared or replaced: deletes its `cms_system_images` row and files. Without this step every replacement or removal left an orphaned row.
- releaseDroppedAssets() : void
- Releases the images that disappeared from the record after the relations were replaced (removed in the gallery or form). Call only after the relations and the main image were written successfully; a shared image (still referenced) is not deleted by {@see releaseAsset()}, and a cleanup error does not fail the save.
- releaseOwnedAssets() : void
- Releases the captured images after the delete commit; a shared image stays ({@see releaseAsset()} → `AssetManager::releaseAssetRow()`), an error is only logged.
- removeEmptyAssetDirs() : void
- Removes the directories the delete left empty (`galleries/20/{big,original,thumb}`, `intros/14/1/…`), walking up while a directory is empty and strictly inside one of the asset roots ({@see assetCleanupRoots()}).
- resolveAssetExtension() : string|null
- Normalized extension of an uploaded file, or null when the format is not allowed in this context.
- saveAssetVariants() : Asset|null
- Saves the files to disk (the format processor does the work) and creates the `Asset` entity with the variant plan and real dimensions. A variant `name` in the database equals the config key and the directory on disk.
Properties
$lastAssetRejectionCode
Why the last {@see saveAssetVariants()} returned `null` ({@see UploadRejection} `REASON_*`); the caller passes it to the report of rejected items. `null` means the last save passed.
protected
string|null
$lastAssetRejectionCode
= null
Methods
assetCleanupRoots()
Asset roots inside which {@see removeEmptyAssetDirs()} may remove empty directories; never above them.
protected
assetCleanupRoots() : array<int, string>
The gallery root by default. A manager writing another channel into its own root (a company logo next to the gallery, an advert attachment next to the images) lists that root too, otherwise the directories of that channel stayed after a delete (admin walkthrough B8).
Return values
array<int, string> —absolute directories
assetDir()
Returns the asset directory from the server neon `params` (through Config), not from `getSettings()`: the shared setting context may be filled by a client without server paths (see the in-process ApiBridge).
protected
assetDir(int $module, string $key, string $type) : string
An empty path is a configuration error and throws loudly instead of silently writing to a relative path.
Requires $this->config on the manager (BaseManager, set by the decorator).
Parameters
- $module : int
-
module in params (1=System, 3=Blog, 5=Bazaar, 6=Comcat, 8=Discussion)
- $key : string
-
directory key (
advertDir,categoryDir,imgDir...) - $type : string
-
absorrel
Return values
stringassetIdOf()
Normalizes the "current image" to an id. Channels with an ORM association return an entity (even a lazy proxy; `getId()` does not initialize it), channels with a scalar FK return an int.
protected
assetIdOf(mixed $current) : int|null
Parameters
- $current : mixed
Return values
int|nullassetVariantFilePaths()
Returns the absolute paths of the files of the asset just written (variants and the original).
protected
assetVariantFilePaths(string $absBase, string $fileName, array<string, mixed> $variantConfigs) : array<int, string>
Variants are written to disk inside the transaction, before it is certain the change holds, and
rollback() knows nothing about files. On a failed path (a Result failure after which SaveTrait
rolls back) the caller therefore deletes what it just wrote (see the orphan variant cleanup in
app/UI/Admin/Eshop/Components/ProductSetEditComponent/Traits/FormTrait.php).
The search is by $fileName (12 random characters), so files of another asset in the same
directory are never touched.
Parameters
- $absBase : string
- $fileName : string
- $variantConfigs : array<string, mixed>
-
key = variant name = subdirectory
Return values
array<int, string>captureOwnedAssetIds()
Ids of the images the given entities own ({@see completedAssetCleanup()} requires the same opt-in `ownedAssetIds()`), captured before the rows are deleted. For paths that bypass {@see DeleteTrait::delete()} and its hooks, such as a subtree delete ({@see TreeTrait::deleteNode()}). Fail-soft: an error of one entity is logged and the others are processed.
protected
captureOwnedAssetIds(iterable<string|int, object> $entities) : array<int, int>
Parameters
- $entities : iterable<string|int, object>
Return values
array<int, int>completedAssetCleanup()
Post-commit cleanup of the assets of an entity that was just deleted.
protected
completedAssetCleanup(array<int, object> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void
DeleteTrait::delete() calls it next to completedDelete()
but under its own name, on purpose: many Api managers define completedDelete() themselves (e.g. Blog
ArticleManager, Bazaar/Comcat AdvertManager, Discussion MessageManager, Eshop ProductSetManager)
and a class method overrides a trait method without any warning, so the shared cleanup would silently
vanish in them.
Opt-in: the hook runs only for managers that define ownedAssetIds(object $entity): array, i.e. that
can tell which assets the entity owns. A generic hook cannot guess it, because the FK carrier differs
(asset / imageId / logoId / photoImageId / a relation collection for a gallery).
Post-commit is right here (unlike releaseAsset() on image replacement): the entity is already
deleted, its FK is gone (SET NULL/CASCADE) and the cleanup must not resurrect it. Inside the
transaction it would still see the FK alive and never release the asset.
Fail-soft: the whole body is in try/catch and returns nothing; a failed cleanup must not bring
back the deleted entity.
IMPORTANT: a shared asset is not judged here; AssetManager::releaseAssetRow()
decides, and when anything still points to the asset it stays with its files (releaseAsset()
returns false).
IMPORTANT: a soft delete does not get here. It goes through save(), not delete(), so this hook does
not run, and it must not: a soft-deleted record can be restored and would come back without its image.
Parameters
- $entities : array<int, object>
-
entities loaded before the delete (
DeleteTraitholds them) - $result : Result
- $meta : array<string, mixed> = []
createAssetRel()
Creates the concrete `*AssetRel` entity linked to the parent and the asset.
protected
abstract createAssetRel(object $parent, Asset $asset, int $order, bool $isMain) : object
The consumer sets it by its entity (setAdvert / setMessage / setFirm).
Parameters
- $parent : object
- $asset : Asset
- $order : int
- $isMain : bool
Return values
objectdeleteAssetFiles()
Deletes the previously scanned files (see {@see listAssetFilesIn()}). Call only after the FK update to the new Asset succeeded.
protected
deleteAssetFiles(array<int, string> $paths) : void
Parameters
- $paths : array<int, string>
dropForeignExistingItems()
From the `type: existing` items keeps only those whose id is already linked to the record `$parentId` through `$relationProperty` (read from the database). Other types (`new`) stay unchanged. When the links cannot be determined (`null`), no existing item passes: losing the order is better than attaching a foreign file.
protected
dropForeignExistingItems(int $parentId, array<int, TItem> $items, string $relationProperty, string $targetAssociation, string $idKey) : array<int, TItem>
Shared by images (assetRels/asset/imageId) and advert files
(fileRels/file/fileId, AdvertAssetTrait::processFiles()).
Parameters
- $parentId : int
- $items : array<int, TItem>
- $relationProperty : string
- $targetAssociation : string
- $idKey : string
Tags
Return values
array<int, TItem>failureOrNull()
A failed Result to propagate, success is null (the `afterSave` hook returns `?Result`).
protected
failureOrNull(Result $result) : Result|null
Parameters
- $result : Result
Return values
Result|nullgetAssetDirAbs()
Absolute prefix of the asset directory. Implement via {@see assetDir()} (reads neon `params` through Config), e.g. `return $this->assetDir(5, 'advertDir', 'abs');`.
protected
abstract getAssetDirAbs() : string
Return values
stringgetAssetDirRel()
Relative prefix of the asset directory. Implement via {@see assetDir()}, e.g. `return $this->assetDir(5, 'advertDir', 'rel');`.
protected
abstract getAssetDirRel() : string
Return values
stringgetAssetFormatKinds()
Media kinds allowed in the dropzone gallery of this manager. The default is images and video. Vector is left out on purpose: an end-user svg is needless exposure even when the sanitizer passes it; enable it explicitly if needed.
protected
getAssetFormatKinds() : array<int, AssetFormatKind>
Return values
array<int, AssetFormatKind>getAssetFormatRegistry()
Requires `$this->assetFormatRegistry` set by the decorator ({@see \App\Core\Base\Api\Models\Managers\Manager::setAssetFormatRegistry()}).
protected
getAssetFormatRegistry() : AssetFormatRegistry
Return values
AssetFormatRegistrygetAssetMaxBytes()
Upper size limit of a new gallery file in bytes, `null` = no limit. The default has none; adverts and company profiles return {@see \App\Core\Security\Upload\ImageUploadSizeLimit::bytes()}.
protected
getAssetMaxBytes() : int|null
Return values
int|nullgetAssetSubDir()
Relative subdirectory within the asset dir (typically Y/m/d/parentId/ or Y/m/parentId/).
protected
abstract getAssetSubDir(int $parentId) : string
Parameters
- $parentId : int
Return values
stringgetAssetUsageKey()
protected
getAssetUsageKey() : string|null
Return values
string|nullgetAssetVariantType()
Key in assetVariants.neon (parameters.assetVariants), e.g. 'bazaar-adverts', 'discussion-messages', 'catalog-firms'.
protected
abstract getAssetVariantType() : string
Return values
stringgetMainAssetColumn()
Name of the property the main (first) gallery image is written to. The default `asset` is an ORM ManyToOne to `Asset` (Message/Advert/Intro). Entities that keep the main image as a flat FK column without an association (Company `Reference::$imageId`) override the hook; otherwise `updateColumn()` would write to a nonexistent property.
protected
getMainAssetColumn() : string
Return values
stringlistAssetFilesIn()
Lists the files lying in the target directory before a new image is written.
protected
listAssetFilesIn(string $absBase) : array<int, string>
Single-image channels (company logo, price list group photo) delete the old file. Deleting it before
the new Asset is saved and the FK updated is a trap: if anything later fails, the database still points
to the old Asset whose files no longer exist. So the old state is only scanned and deleted after
success (see deleteAssetFiles()). Name collisions cannot happen because saveAssetVariants()
generates a random fileName, so the new image lies next to the old one.
Parameters
- $absBase : string
Return values
array<int, string> —absolute paths
orderedAssetsFromMeta()
Turns `meta['orderedAssets']` of the request into the list of dropzone items in the shape {@see processOrderedAssets()} reads. Unknown keys and values of a wrong type are dropped; a present but non-text `path` stays as `''` so the file read rejects it the same way instead of silently skipping it.
protected
static orderedAssetsFromMeta(mixed $value) : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
Parameters
- $value : mixed
Return values
array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>orderedFilesFromMeta()
Turns `meta['files']` of the request into the list of files for `processFiles()`/`processComplaintFiles()`.
protected
static orderedFilesFromMeta(mixed $value) : array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>
An item without a text type is dropped (processing cannot place it anyway); other keys as in
orderedAssetsFromMeta().
Parameters
- $value : mixed
Return values
array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>processOrderedAssets()
Shared template method for processing an ordered array of dropzone assets.
protected
processOrderedAssets(object $entity, array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}> $orderedAssets) : Result|null
IMPORTANT: every database step (soft-remove rels, full replace of rels, main image FK) returns a
Result; a failure is returned to the caller and the next step is skipped. The caller (afterSave)
must pass it on so that SaveTrait::save() rolls back the whole save; otherwise a gallery error is
dropped and the admin gets success with an unchanged or half-changed gallery. null means the gallery
is unchanged or everything passed.
Parameters
- $entity : object
- $orderedAssets : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
Return values
Result|nullreleaseAsset()
Releases the old asset after an image was cleared or replaced: deletes its `cms_system_images` row and files. Without this step every replacement or removal left an orphaned row.
protected
releaseAsset(int|object|null $current[, int|null $keepAssetId = null ]) : bool
IMPORTANT: call only after a successful updateColumn(), i.e. when the FK no longer points to $assetId.
Shared assets: this method judges nothing, AssetManager::releaseAssetRow()
decides; it derives the references from information_schema and deletes nothing when in doubt.
It runs inside the save transaction of the caller (afterSave), not post-commit:
- the row delete is atomic with clearing or switching the FK; if anything later in
afterSavefails, the rollback restores both, while a post-commit variant is irreversible, - the reference check runs on the same connection, so it sees the FK just written (a post-commit hook would have to read again and could miss another edit),
- several affected managers already define the post-commit
completedSave()themselves (BlogArticleManager, DiscussionMessageManager, EshopProductSetManager), so a trait version would silently not run in them. Files are deleted right after the row, wheredeleteAssetFiles($stalePaths)already stands for a replacement, so no new window appears.
Fail-soft: any cleanup error is only logged (channel assets) and the main save does not fail;
an orphaned row is a smaller defect than a broken edit.
Parameters
- $current : int|object|null
-
old FK (id or
Assetentity); null or 0 means nothing to release - $keepAssetId : int|null = null
-
id that must not be released (the newly written asset)
Return values
boolreleaseDroppedAssets()
Releases the images that disappeared from the record after the relations were replaced (removed in the gallery or form). Call only after the relations and the main image were written successfully; a shared image (still referenced) is not deleted by {@see releaseAsset()}, and a cleanup error does not fail the save.
protected
releaseDroppedAssets(array<int, int> $previousAssetIds, array<int, int> $keptAssetIds) : void
Parameters
- $previousAssetIds : array<int, int>
-
images of the record before the save
- $keptAssetIds : array<int, int>
-
images the record holds after the save
releaseOwnedAssets()
Releases the captured images after the delete commit; a shared image stays ({@see releaseAsset()} → `AssetManager::releaseAssetRow()`), an error is only logged.
protected
releaseOwnedAssets(array<int, int> $assetIds) : void
Parameters
- $assetIds : array<int, int>
removeEmptyAssetDirs()
Removes the directories the delete left empty (`galleries/20/{big,original,thumb}`, `intros/14/1/…`), walking up while a directory is empty and strictly inside one of the asset roots ({@see assetCleanupRoots()}).
protected
removeEmptyAssetDirs(array<int, string> $dirs) : void
Without it every deleted gallery or intro left its empty per-record folders behind. The root itself
and anything outside it are never touched; a directory that is not empty (another asset, a race
with a new upload) simply stays, rmdir() refuses it. A failure is logged, never thrown.
Parameters
- $dirs : array<int, string>
-
directories of the deleted files
resolveAssetExtension()
Normalized extension of an uploaded file, or null when the format is not allowed in this context.
protected
resolveAssetExtension(string $fileName[, array<int, AssetFormatKind> $kinds = [AssetFormatKind::Raster] ]) : string|null
Replaces copied whitelists with a silent fallback to jpg, which would save an svg under the
extension .jpg with xml content.
Parameters
- $fileName : string
- $kinds : array<int, AssetFormatKind> = [AssetFormatKind::Raster]
Return values
string|nullsaveAssetVariants()
Saves the files to disk (the format processor does the work) and creates the `Asset` entity with the variant plan and real dimensions. A variant `name` in the database equals the config key and the directory on disk.
protected
saveAssetVariants(string $rawData, string $extension, string $absBase, string $relBase, array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array, useWatermark: bool}> $variantConfigs[, array<int, AssetFormatKind> $kinds = [AssetFormatKind::Raster] ]) : Asset|null
The variant plan is always stored, even when the variants are not on disk yet (video without ffmpeg),
so the catch-up cron knows what to add; reality is held by the flag thumbnails_generated.
Returns null when the format is unsupported or the processor rejected it (corrupt image, svg that failed sanitization). The caller skips such a file.
Parameters
- $rawData : string
- $extension : string
- $absBase : string
- $relBase : string
-
$variantConfigs
: array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array
, useWatermark: bool}> - $kinds : array<int, AssetFormatKind> = [AssetFormatKind::Raster]
-
media kinds allowed in this context