CoolCMS API

AssetVariantBackfill
in package

FinalYes

Pure logic of regenerating missing image variants and deleting originals. No disk or DB, so it is covered by a unit test ({@see \Tests\Unit\System\AssetVariantBackfillTest}). Disk access (file existence, dimensions) and writes come from {@see \App\UI\Api\System\Models\Managers\AssetVariantRegenerationManager}.

  • decide(): usage plan x record x files on disk gives what to create and why something is skipped;
  • needsNewFile(): whether an image counts as regenerable (only when a new file would be created);
  • applyToRecord(): adds new folders to variants (the original stays last);
  • canDeleteOriginal() / withoutOriginal(): original deletion rules;
  • usageKeyFromPath(): usage of an old image derived from its path;
  • classify(): three states per usage: missing current variants, failed to generate, not regenerable;
  • staleFolders() / withOverwrite(): files whose dimensions differ from today's plan;
  • outdatedFolders() / withoutFolders(): folders the record has and the plan does not.

Table of Contents

Constants

BLOCKED  : string = 'blocked'
COMPLETE  : string = 'complete'
States of {@see decide()}.
EXHAUSTED  : string = 'exhausted'
MAX_ATTEMPTS  : int = 3
After this many failed attempts an image is skipped.
PENDING  : string = 'pending'
PROTECTED_FOLDERS  : array<string|int, mixed> = ['big', 'thumb', 'original']
Folders that cleanup of old variants must never delete, neither as a folder nor as an alias.
SKIP_NO_SOURCE  : string = 'noSource'
SKIP_TOO_LARGE  : string = 'tooLarge'
Reasons a variant is skipped.
SKIP_WATERMARKED_SOURCE  : string = 'watermarkedSource'
PATH_USAGE  : array<string|int, mixed> = ['blog/images/articles' => 'blog-articles', 'bl...
Part of the path `/modules/<module>/images/<folder>/` to usage key. The roots are `params` from `config/Shared/parameters.neon` (`assetDir`); a new usage with its own root belongs here too, otherwise its old images stay without `usage_key`.
RASTER_EXTENSIONS  : array<string|int, mixed> = ['jpg', 'jpeg', 'png', 'gif', 'webp']
Extensions the raster processor handles; video and SVG are not regenerated.

Methods

applyToRecord()  : array<int, array<string, mixed>>
Adds new folders to the record `variants` in the shape of a new upload (`name`, dimensions, flags, watermark, `variant`, `alias`). An existing item of the same folder is replaced in place, new ones go before the original, which stays last.
canDeleteOriginal()  : bool
Whether the original of an image may be deleted. Only raster, only when both the record and the disk have it and the `big` folder file exists (record and disk); without `big` nothing would be left to display after the deletion.
canDeleteOutdated()  : bool
Whether old variants of an image may be cleaned up. Only raster and only when the original is on disk; a deleted downscale could no longer be created again.
classify()  : array{missing: bool, failed: bool, unfixable: bool}
Three states of an image against the usage plan. An image can be in more than one.
decide()  : array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array}
Decides what to regenerate for an image.
expectedSize()  : array{width: int, height: int}|null
Dimensions the image `$srcWidth` x `$srcHeight` would get under the variant configuration; the same calculation as `VariantRenderer::renderVariants()` (Nette `Image::resize()`): a variant without shrinking is a raw copy, height 0 means by width only without enlarging, `Cover` crops to exact dimensions. A watermark does not change the size. Returns `null` when the source dimensions are unusable.
isRaster()  : bool
Whether the extension is a raster image (jpg/jpeg/png/gif/webp); video and SVG are not.
likePatterns()  : array<string, string>
Patterns for SQL `LIKE` (root `.../modules/<module>/images/<folder>/...`) to usage key; used for bulk filling of `usage_key`. Roots contain no `%` or `_`, so nothing is escaped.
needsNewFile()  : bool
Whether regenerating the image would create a new variant file; only that counts as regenerable.
outdatedFolders()  : array<int, string>
Folders the image record has but the usage plan does not: cleanup candidates. Never `big`, `thumb` or `original`: both the folder and the record alias are protected. A plan without a single downscale (a usage without assigned variants, produced from the neon fallback) cleans nothing, otherwise everything would be deleted. Also never deleted are folders and aliases from `$requiredAliases` (required aliases of the usage, e.g. favicon `icon32/icon180/icon512`, operator `avatar`), which templates read even when today's plan lacks them.
rasterExtensions()  : array<int, string>
Raster image extensions for an SQL filter.
staleFolders()  : array<int, string>
Plan folders whose existing file has different dimensions than the original would give today.
usageKeyFromPath()  : string|null
Usage derived from the image path (`/www/modules/blog/images/articles/5/1/`). Only when the path clearly belongs to one root; otherwise `null` (a guess is never written).
withoutFolders()  : array{variants: list, meta: array}
Record without the given folders: the items of `variants` and `meta.variants` are removed. `big`, `thumb` and `original` (and required aliases of the usage from `$requiredAliases`) are never removed, even when the caller lists them.
withoutOriginal()  : array{variants: list>, meta: array}
Record without the original: the `original` item of `variants` and `meta.variants.original` are removed.
withOverwrite()  : array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}
Adds folders to overwrite to the decision (regenerate even existing files). Overwriting happens only with an original (the caller supplies `stale` only then) and, like regeneration, respects the attempt limit.
__construct()  : mixed
isOriginalEntry()  : bool
number()  : int
protectedNames()  : array<int, string>
Untouchable folder and alias names during cleanup: `big`, `thumb`, `original` + required aliases of the usage.
text()  : string

Constants

MAX_ATTEMPTS

After this many failed attempts an image is skipped.

public int MAX_ATTEMPTS = 3

PROTECTED_FOLDERS

Folders that cleanup of old variants must never delete, neither as a folder nor as an alias.

public array<string|int, mixed> PROTECTED_FOLDERS = ['big', 'thumb', 'original']

PATH_USAGE

Part of the path `/modules/<module>/images/<folder>/` to usage key. The roots are `params` from `config/Shared/parameters.neon` (`assetDir`); a new usage with its own root belongs here too, otherwise its old images stay without `usage_key`.

private array<string|int, mixed> PATH_USAGE = ['blog/images/articles' => 'blog-articles', 'blog/images/events' => 'blog-events', 'blog/images/flash-messages' => 'blog-flash-messages', 'blog/images/intros' => 'blog-intros', 'blog/images/galleries' => 'blog-galleries', 'blog/images/categories' => 'blog-categories', 'eshop/images/products' => 'eshop-products', 'eshop/images/product-sets' => 'eshop-product-sets', 'eshop/images/categories' => 'eshop-categories', 'eshop/images/parameters' => 'eshop-parameters', 'bazaar/images/items' => 'bazaar-adverts', 'bazaar/images/categories' => 'bazaar-categories', 'bazaar/images/parameters' => 'bazaar-parameters', 'comcat/images/items' => 'comcat-adverts', 'comcat/images/categories' => 'comcat-categories', 'discussion/images/messages' => 'discussion-messages', 'discussion/images/categories' => 'discussion-categories', 'company/images/logos' => 'company-logos', 'company/images/photos' => 'company-photos', 'company/images/references' => 'company-references', 'company/images/reference-logos' => 'company-reference-logos', 'company/images/service-groups' => 'company-service-groups', 'company/images/team' => 'company-team', 'company/images/categories' => 'company-categories']

RASTER_EXTENSIONS

Extensions the raster processor handles; video and SVG are not regenerated.

private array<string|int, mixed> RASTER_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']

Methods

applyToRecord()

Adds new folders to the record `variants` in the shape of a new upload (`name`, dimensions, flags, watermark, `variant`, `alias`). An existing item of the same folder is replaced in place, new ones go before the original, which stays last.

public static applyToRecord(array<int, array<string, mixed>> $variants, array<string, array<string, mixed>> $added) : array<int, array<string, mixed>>
Parameters
$variants : array<int, array<string, mixed>>

current variants of the record

$added : array<string, array<string, mixed>>

folder to configuration from the plan

Return values
array<int, array<string, mixed>>

canDeleteOriginal()

Whether the original of an image may be deleted. Only raster, only when both the record and the disk have it and the `big` folder file exists (record and disk); without `big` nothing would be left to display after the deletion.

public static canDeleteOriginal(array<int, array{folder: string, original: bool}> $files, string|null $extension, bool $originalOnDisk, bool $bigOnDisk) : bool
Parameters
$files : array<int, array{folder: string, original: bool}>

output of Asset::getVariantFiles()

$extension : string|null
$originalOnDisk : bool
$bigOnDisk : bool
Return values
bool

canDeleteOutdated()

Whether old variants of an image may be cleaned up. Only raster and only when the original is on disk; a deleted downscale could no longer be created again.

public static canDeleteOutdated(string|null $extension, bool $originalOnDisk) : bool
Parameters
$extension : string|null
$originalOnDisk : bool
Return values
bool

classify()

Three states of an image against the usage plan. An image can be in more than one.

public static classify(array<string, array<string, mixed>> $plan, array<int, string> $recordFolders, array<int, string> $existing, bool $hasOriginal, array<string, array{width: int, watermark: bool}> $sources, int $attempts) : array{missing: bool, failed: bool, unfixable: bool}
  • missing: no current variants. The plan has a folder the record does not know, the file is not on disk and it can be created (from the original or a larger downscale without a watermark). Not an error: older images work without regeneration;
  • failed: failed to generate. The record lists the folder but the file is missing, or variants_attempts > 0 (an attempt failed);
  • unfixable: not regenerable. The file is missing and there is nothing to create it from (without an original the target is larger than the largest downscale without a watermark, or there is no downscale).

An image missing only the record entry for an existing file (reconcile) counts nowhere. attempts does not affect whether a file can be created (exhausted attempts do not remove the image from missing; the retry action handles them). A missing original folder is a source, not a target.

Parameters
$plan : array<string, array<string, mixed>>

folder to configuration from the plan

$recordFolders : array<int, string>

folders from the record variants

$existing : array<int, string>

plan folders (without the original) whose file is on disk

$hasOriginal : bool
$sources : array<string, array{width: int, watermark: bool}>

existing downscales with a known width

$attempts : int
Return values
array{missing: bool, failed: bool, unfixable: bool}

decide()

Decides what to regenerate for an image.

public static decide(array<string, array<string, mixed>> $plan, array<int, string> $recordFolders, array<int, string> $existing, bool $hasOriginal, array<string, array{width: int, watermark: bool}> $sources, int $attempts) : array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array}

The plan is the provider's folder map; the original key (the usage stores the original) is never regenerated: it is a source, not a target. When the original file is on disk but the record does not know it (an old image), it is added to the record (reconcile). A missing folder file is created from the original; without an original, from the largest existing downscale, but only variants with a smaller or equal target width (never an upscale) and never from a watermarked variant (the watermark would double or leak into a variant without it). An existing file is not overwritten even when the variant dimensions changed meanwhile.

A folder whose file is on disk but the record does not know it (e.g. an interrupted earlier run) goes to reconcile: only the record is completed.

Parameters
$plan : array<string, array<string, mixed>>

folder to variant configuration (maxWidth, ...)

$recordFolders : array<int, string>

folders from the record variants

$existing : array<int, string>

folders (without the original) whose file is on disk

$hasOriginal : bool
$sources : array<string, array{width: int, watermark: bool}>

existing downscales with a known width

$attempts : int
Return values
array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array}

expectedSize()

Dimensions the image `$srcWidth` x `$srcHeight` would get under the variant configuration; the same calculation as `VariantRenderer::renderVariants()` (Nette `Image::resize()`): a variant without shrinking is a raw copy, height 0 means by width only without enlarging, `Cover` crops to exact dimensions. A watermark does not change the size. Returns `null` when the source dimensions are unusable.

public static expectedSize(int $srcWidth, int $srcHeight, array<string, mixed> $cfg) : array{width: int, height: int}|null
Parameters
$srcWidth : int
$srcHeight : int
$cfg : array<string, mixed>

configuration from the plan (maxWidth, maxHeight, resizeFlags)

Return values
array{width: int, height: int}|null

isRaster()

Whether the extension is a raster image (jpg/jpeg/png/gif/webp); video and SVG are not.

public static isRaster(string|null $extension) : bool
Parameters
$extension : string|null
Return values
bool

likePatterns()

Patterns for SQL `LIKE` (root `.../modules/<module>/images/<folder>/...`) to usage key; used for bulk filling of `usage_key`. Roots contain no `%` or `_`, so nothing is escaped.

public static likePatterns() : array<string, string>
Return values
array<string, string>

needsNewFile()

Whether regenerating the image would create a new variant file; only that counts as regenerable.

public static needsNewFile(array{state: string, create: list} $decision) : bool

Regeneration is optional: older images need not have all variants of new ones and templates offer only what the image has. An image missing only the record entry for a folder that is on disk (reconcile, e.g. original/ without an item in variants) needs nothing; the batch completes the record when it runs for another reason.

Parameters
$decision : array{state: string, create: list}

output of decide()

Return values
bool

outdatedFolders()

Folders the image record has but the usage plan does not: cleanup candidates. Never `big`, `thumb` or `original`: both the folder and the record alias are protected. A plan without a single downscale (a usage without assigned variants, produced from the neon fallback) cleans nothing, otherwise everything would be deleted. Also never deleted are folders and aliases from `$requiredAliases` (required aliases of the usage, e.g. favicon `icon32/icon180/icon512`, operator `avatar`), which templates read even when today's plan lacks them.

public static outdatedFolders(array<int, mixed> $variants, array<int, string> $planFolders[, array<int, string> $requiredAliases = [] ]) : array<int, string>
Parameters
$variants : array<int, mixed>

items of the record variants (from JSON, shape not guaranteed)

$planFolders : array<int, string>

folders of today's plan

$requiredAliases : array<int, string> = []

required aliases of the usage (AssetVariantPlan::requiredAliasesFor())

Return values
array<int, string> —

folders (lowercase) to delete

rasterExtensions()

Raster image extensions for an SQL filter.

public static rasterExtensions() : array<int, string>
Return values
array<int, string>

staleFolders()

Plan folders whose existing file has different dimensions than the original would give today.

public static staleFolders(array<string, array<string, mixed>> $plan, array<string, array{width: int, height: int}|null> $actual, array{width: int, height: int}|null $original) : array<int, string>

A folder with unknown actual dimensions is not overwritten (nothing to compare); original is not a target.

Parameters
$plan : array<string, array<string, mixed>>

folder to configuration from the plan

$actual : array<string, array{width: int, height: int}|null>

actual dimensions of existing files (from meta, otherwise from the file header); a folder without a file is absent

$original : array{width: int, height: int}|null

original dimensions

Return values
array<int, string>

usageKeyFromPath()

Usage derived from the image path (`/www/modules/blog/images/articles/5/1/`). Only when the path clearly belongs to one root; otherwise `null` (a guess is never written).

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

withoutFolders()

Record without the given folders: the items of `variants` and `meta.variants` are removed. `big`, `thumb` and `original` (and required aliases of the usage from `$requiredAliases`) are never removed, even when the caller lists them.

public static withoutFolders(array<int, mixed> $variants, array<string, mixed> $meta, array<int, string> $folders[, array<int, string> $requiredAliases = [] ]) : array{variants: list, meta: array}
Parameters
$variants : array<int, mixed>
$meta : array<string, mixed>
$folders : array<int, string>
$requiredAliases : array<int, string> = []

required aliases of the usage

Return values
array{variants: list, meta: array}

withoutOriginal()

Record without the original: the `original` item of `variants` and `meta.variants.original` are removed.

public static withoutOriginal(array<int, array<string, mixed>> $variants, array<string, mixed> $meta) : array{variants: list>, meta: array}
Parameters
$variants : array<int, array<string, mixed>>
$meta : array<string, mixed>
Return values
array{variants: list>, meta: array}

withOverwrite()

Adds folders to overwrite to the decision (regenerate even existing files). Overwriting happens only with an original (the caller supplies `stale` only then) and, like regeneration, respects the attempt limit.

public static withOverwrite(array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array} $decision, array<int, string> $stale, int $attempts) : array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}
Parameters
$decision : array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array}

output of decide()

$stale : array<int, string>

output of staleFolders()

$attempts : int
Return values
array{state: string, create: list, reconcile: list, source: array{kind: string, folder: string|null, width: int|null}|null, skipped: array, overwrite: list}

isOriginalEntry()

private static isOriginalEntry(mixed $entry) : bool
Parameters
$entry : mixed

item of variants (from JSON, shape not guaranteed)

Return values
bool

number()

private static number(mixed $value) : int
Parameters
$value : mixed
Return values
int

protectedNames()

Untouchable folder and alias names during cleanup: `big`, `thumb`, `original` + required aliases of the usage.

private static protectedNames(array<int, string> $requiredAliases) : array<int, string>
Parameters
$requiredAliases : array<int, string>
Return values
array<int, string> —

lowercase

text()

private static text(mixed $value) : string
Parameters
$value : mixed
Return values
string
On this page

Search results