CoolCMS API

AssetVariantRegenerationManager
in package

FinalYes

Backfills missing image variants and deletes stored originals.

A process orchestrator without its own table: it reads and writes image rows via AssetManager, usages via AssetUsageManager and the plan via AssetVariantProvider. What to create and why to skip is decided by the pure AssetVariantBackfill::decide().

  • findStats(): per usage, three counts (no current variants, generation failed, cannot be backfilled), plus how many originals and outdated variants can be deleted;
  • regenerate(): one backfill batch (lock, attempts counter); optionally overwrites files whose size differs from today's plan and resets the attempts ("try again");
  • deleteOriginals(): deletes stored originals of usages that no longer keep the original;
  • deleteOutdatedVariants(): deletes variants the record has but the usage plan no longer has.

Only raster images are backfilled (video has its own catch-up cron, SVG has nothing to shrink), and only missing folders; an existing file is overwritten only on explicit request and only from the original. The source is the original, without it the largest shrunk file, and only smaller targets are made.

IMPORTANT: no RBAC inside; the permission boundary is the Api presenter (asset-usage).

Table of Contents

Constants

DEFAULT_BATCH  : int = 25
Images per batch per usage.
DELETE_BATCH  : int = 500
Originals deleted per click (the confirmation shows the total count and size).
LOCK_NAME  : string = 'asset.variants.regenerate'
Batch lock shared by the cron and the admin button, so they never run concurrently.
MAX_BATCH  : int = 500
Upper bound of the batch size a caller may request (cron `limit`).
LOCK_TTL  : int = 300
Lock TTL; the batch extends it after every image.
PAGE_SIZE  : int = 200
Rows loaded at once while iterating the images of a usage.
TIME_BUDGET  : int = 45
Time budget of one batch in seconds; both cron and HTTP requests have a cap.

Properties

$assetManager  : AssetManager
$cronLockManager  : CronLockManager
$usageManager  : AssetUsageManager
$variantProvider  : AssetVariantProvider
$variantRenderer  : VariantRenderer

Methods

__construct()  : mixed
deleteOriginals()  : Result
Deletes stored originals of a usage that no longer keeps the original. Raster images only, and only when both the record and the disk have the original and `big`. Removes `original` from `variants` and `meta`. One call deletes at most {@see DELETE_BATCH} files; the rest is reported in `remaining`.
deleteOutdatedVariants()  : Result
Deletes variants the image record has but today's usage plan no longer has. Never `big`, `thumb` or `original`; raster images with an original on disk only. Updates `variants` and `meta`, deletes the file and the empty folder. One call deletes files of at most {@see DELETE_BATCH} images; the rest is reported in `remaining`.
findStats()  : array<int, array{usageId: int, usageKey: string, moduleId: int, keepOriginal: bool, originals: array{count: int, bytes: int}|null, outdated: array{images: int, files: int, bytes: int}, total: int, missing: int, failed: int, unfixable: int}>
Returns the state of images per usage (optionally of one module). Read-only: old images without `usage_key` are assigned by path in memory only, nothing is written to the DB.
regenerate()  : Result
Runs one backfill batch. `$usageId = null` means all usages (cron), otherwise one (button).
absBase()  : string|null
Absolute image folder with a trailing slash; `null` when the path is outside the website or invalid.
buildProbe()  : Asset
Builds a non-persisted entity only for reading the record (`getVariantFiles()` etc.); it is never saved.
collectSources()  : array<string, array{width: int, watermark: bool}>
Returns existing shrunk files usable as a source: the file is on disk and its width is known (from `meta`, otherwise from the file header). The original is excluded.
eachRow()  : Generator<int, array<string, mixed>>
Yields raster image rows of one usage (`null` = without usage), paged by id.
evaluate()  : Asset, absBase: string, ext: string, fileName: string, classes: array{missing: bool, failed: bool, unfixable: bool}, decision: array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}}
Compares the plan with the record and disk of one image. `$overwrite` also finds folders to overwrite (the file exists but its size differs from what would be made from the original today); sizes come from `meta`, otherwise from the file header.
filePath()  : string
findDeletableOriginal()  : Asset, path: string, bytes: int}|null
Returns the deletable original (path, size and probe entity), or `null`.
findOutdated()  : Asset, folders: list, files: list, bytes: int}|null
Returns the old variants that can be deleted: record folders outside today's plan whose file is on disk. Raster images with an original on disk only; never `big`, `thumb`, `original` or the required aliases of the usage (folder or alias). Both the statistics ({@see tally()}) and the deletion use this single path, so the confirmation matches what is deleted.
findStale()  : array<int, string>
Returns folders whose file exists but whose size differs from today's plan. Real sizes come from `meta`, otherwise from the file header; the same for the original.
imageSize()  : array{width: int, height: int}|null
int()  : int
isKeptOriginal()  : bool
planFolders()  : array<string, array<string, mixed>>
Returns the usage plan as folder => config; `original` only when the usage stores it. The original is a source, not a target: it is not backfilled, only added to the record based on the disk (see {@see AssetVariantBackfill::decide()}).
processImage()  : array{created: int, overwritten: int}|null
Creates missing files, overwrites files whose size differs from the plan and writes the record.
renderAtomically()  : array<string, array{width: int, height: int}>
Renders the variants into temporary files in the same folders (`.regen-<random>-<name>.<ext>`) and moves them into place (`rename`) only after all of them rendered. On failure no broken file or half-overwritten variant remains and the temporary files are cleaned up.
tally()  : void
text()  : string
variantEntries()  : array<int, array<string, mixed>>
Returns the `variants` entries of the record (from JSON, shape not guaranteed, hence `mixed` values).

Constants

LOCK_NAME

Batch lock shared by the cron and the admin button, so they never run concurrently.

public string LOCK_NAME = 'asset.variants.regenerate'

Properties

Methods

deleteOriginals()

Deletes stored originals of a usage that no longer keeps the original. Raster images only, and only when both the record and the disk have the original and `big`. Removes `original` from `variants` and `meta`. One call deletes at most {@see DELETE_BATCH} files; the rest is reported in `remaining`.

public deleteOriginals(int $usageId) : Result
Parameters
$usageId : int
Return values
Result —

data: deleted, bytes (freed), failed, remaining

deleteOutdatedVariants()

Deletes variants the image record has but today's usage plan no longer has. Never `big`, `thumb` or `original`; raster images with an original on disk only. Updates `variants` and `meta`, deletes the file and the empty folder. One call deletes files of at most {@see DELETE_BATCH} images; the rest is reported in `remaining`.

public deleteOutdatedVariants(int $usageId) : Result
Parameters
$usageId : int
Return values
Result —

data: deleted (files), bytes (freed), failed, remaining (images)

findStats()

Returns the state of images per usage (optionally of one module). Read-only: old images without `usage_key` are assigned by path in memory only, nothing is written to the DB.

public findStats([int|null $moduleId = null ]) : array<int, array{usageId: int, usageKey: string, moduleId: int, keepOriginal: bool, originals: array{count: int, bytes: int}|null, outdated: array{images: int, files: int, bytes: int}, total: int, missing: int, failed: int, unfixable: int}>

The counts missing, failed, unfixable count images (an image can fall into more than one, see AssetVariantBackfill::classify()); missing is not an error, older images work without backfilling. outdated = old variants to delete (images, files, bytes).

Parameters
$moduleId : int|null = null
Return values
array<int, array{usageId: int, usageKey: string, moduleId: int, keepOriginal: bool, originals: array{count: int, bytes: int}|null, outdated: array{images: int, files: int, bytes: int}, total: int, missing: int, failed: int, unfixable: int}>

regenerate()

Runs one backfill batch. `$usageId = null` means all usages (cron), otherwise one (button).

public regenerate([int|null $usageId = null ][, int $batch = self::DEFAULT_BATCH ][, bool $overwrite = false ][, bool $retry = false ]) : Result

First fills usage_key of old images by path, then processes at most $batch images per usage.

A failed attempt (unreadable source, render exception) increments variants_attempts; after AssetVariantBackfill::MAX_ATTEMPTS the image is skipped until the attempts are reset ($retry, single usage only, the "try again" button). A successful run resets the attempts.

$overwrite additionally overwrites variant files whose real size differs from what today's plan would produce from the original; only for images with an original, written atomically (temporary file + rename).

Parameters
$usageId : int|null = null
$batch : int = self::DEFAULT_BATCH
$overwrite : bool = false
$retry : bool = false
Return values
Result —

data: processed (images processed), created (files created), overwritten (files overwritten), failed, skipped (images that cannot be backfilled), remaining (waiting for the next batch), keysFilled, reset (images with reset attempts)

absBase()

Absolute image folder with a trailing slash; `null` when the path is outside the website or invalid.

private absBase(string $relPath) : string|null
Parameters
$relPath : string
Return values
string|null

buildProbe()

Builds a non-persisted entity only for reading the record (`getVariantFiles()` etc.); it is never saved.

private buildProbe(array<string, mixed> $row) : Asset
Parameters
$row : array<string, mixed>
Return values
Asset

collectSources()

Returns existing shrunk files usable as a source: the file is on disk and its width is known (from `meta`, otherwise from the file header). The original is excluded.

private collectSources(Asset $asset, string $absBase, string $ext, array<string, array<string, mixed>> $plan, array<string, array<string, mixed>> $records) : array<string, array{width: int, watermark: bool}>
Parameters
$asset : Asset
$absBase : string
$ext : string
$plan : array<string, array<string, mixed>>
$records : array<string, array<string, mixed>>

variants entries by folder

Return values
array<string, array{width: int, watermark: bool}>

eachRow()

Yields raster image rows of one usage (`null` = without usage), paged by id.

private eachRow(string|null $usageKey) : Generator<int, array<string, mixed>>
Parameters
$usageKey : string|null
Return values
Generator<int, array<string, mixed>>

evaluate()

Compares the plan with the record and disk of one image. `$overwrite` also finds folders to overwrite (the file exists but its size differs from what would be made from the original today); sizes come from `meta`, otherwise from the file header.

private evaluate(array<string, mixed> $row, array<string, array<string, mixed>> $plan[, bool $overwrite = false ]) : Asset, absBase: string, ext: string, fileName: string, classes: array{missing: bool, failed: bool, unfixable: bool}, decision: array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}}
Parameters
$row : array<string, mixed>
$plan : array<string, array<string, mixed>>
$overwrite : bool = false
Return values
Asset, absBase: string, ext: string, fileName: string, classes: array{missing: bool, failed: bool, unfixable: bool}, decision: array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}}

filePath()

private filePath(string $absBase, string $folder, string $fileName, string $ext) : string
Parameters
$absBase : string
$folder : string
$fileName : string
$ext : string
Return values
string

findDeletableOriginal()

Returns the deletable original (path, size and probe entity), or `null`.

private findDeletableOriginal(array<string, mixed> $row) : Asset, path: string, bytes: int}|null
Parameters
$row : array<string, mixed>
Return values
Asset, path: string, bytes: int}|null

findOutdated()

Returns the old variants that can be deleted: record folders outside today's plan whose file is on disk. Raster images with an original on disk only; never `big`, `thumb`, `original` or the required aliases of the usage (folder or alias). Both the statistics ({@see tally()}) and the deletion use this single path, so the confirmation matches what is deleted.

private findOutdated(array<string, mixed> $row, array<string, array<string, mixed>> $plan, string $usageKey) : Asset, folders: list, files: list, bytes: int}|null
Parameters
$row : array<string, mixed>
$plan : array<string, array<string, mixed>>
$usageKey : string
Return values
Asset, folders: list, files: list, bytes: int}|null

findStale()

Returns folders whose file exists but whose size differs from today's plan. Real sizes come from `meta`, otherwise from the file header; the same for the original.

private findStale(Asset $asset, string $absBase, string $ext, array<string, array<string, mixed>> $plan, array<int, string> $existing) : array<int, string>
Parameters
$asset : Asset
$absBase : string
$ext : string
$plan : array<string, array<string, mixed>>
$existing : array<int, string>

plan folders with an existing file

Return values
array<int, string>

imageSize()

private imageSize(string $path) : array{width: int, height: int}|null
Parameters
$path : string
Return values
array{width: int, height: int}|null

isKeptOriginal()

private isKeptOriginal(array<int, array{usageId: int, usageKey: string, moduleId: int, keepOriginal: bool, position: int}> $usages, string $usageKey) : bool
Parameters
$usages : array<int, array{usageId: int, usageKey: string, moduleId: int, keepOriginal: bool, position: int}>
$usageKey : string
Return values
bool

planFolders()

Returns the usage plan as folder => config; `original` only when the usage stores it. The original is a source, not a target: it is not backfilled, only added to the record based on the disk (see {@see AssetVariantBackfill::decide()}).

private planFolders(string $usageKey) : array<string, array<string, mixed>>
Parameters
$usageKey : string
Return values
array<string, array<string, mixed>>

processImage()

Creates missing files, overwrites files whose size differs from the plan and writes the record.

private processImage(Asset, absBase: string, ext: string, fileName: string, decision: array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}} $eval, array<string, array<string, mixed>> $plan) : array{created: int, overwritten: int}|null

Returns the counts of created and overwritten files, or null on failure (the attempts counter is then incremented). Success resets the counter.

Parameters
$eval : Asset, absBase: string, ext: string, fileName: string, decision: array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}}
$plan : array<string, array<string, mixed>>
Return values
array{created: int, overwritten: int}|null

renderAtomically()

Renders the variants into temporary files in the same folders (`.regen-<random>-<name>.<ext>`) and moves them into place (`rename`) only after all of them rendered. On failure no broken file or half-overwritten variant remains and the temporary files are cleaned up.

private renderAtomically(string $bytes, string $fileName, string $ext, string $absBase, array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array, useWatermark: bool}> $configs) : array<string, array{width: int, height: int}>
Parameters
$bytes : string
$fileName : string
$ext : string
$absBase : string
$configs : array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array, useWatermark: bool}>
Return values
array<string, array{width: int, height: int}>

tally()

private tally(array{counts: array{total: int, missing: int, failed: int, unfixable: int}, originals: array{count: int, bytes: int}, outdated: array{images: int, files: int, bytes: int}} &$acc, bool $countOriginals, array<string, mixed> $row, array<string, array<string, mixed>> $plan, string $usageKey) : void
Parameters
$acc : array{counts: array{total: int, missing: int, failed: int, unfixable: int}, originals: array{count: int, bytes: int}, outdated: array{images: int, files: int, bytes: int}}
$countOriginals : bool
$row : array<string, mixed>
$plan : array<string, array<string, mixed>>
$usageKey : string

variantEntries()

Returns the `variants` entries of the record (from JSON, shape not guaranteed, hence `mixed` values).

private variantEntries(Asset $asset) : array<int, array<string, mixed>>
Parameters
$asset : Asset
Return values
array<int, array<string, mixed>>
On this page

Search results