AssetVariantBackfill
in package
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
BLOCKED
public
string
BLOCKED
= 'blocked'
COMPLETE
States of {@see decide()}.
public
string
COMPLETE
= 'complete'
EXHAUSTED
public
string
EXHAUSTED
= 'exhausted'
MAX_ATTEMPTS
After this many failed attempts an image is skipped.
public
int
MAX_ATTEMPTS
= 3
PENDING
public
string
PENDING
= 'pending'
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']
SKIP_NO_SOURCE
public
string
SKIP_NO_SOURCE
= 'noSource'
SKIP_TOO_LARGE
Reasons a variant is skipped.
public
string
SKIP_TOO_LARGE
= 'tooLarge'
SKIP_WATERMARKED_SOURCE
public
string
SKIP_WATERMARKED_SOURCE
= 'watermarkedSource'
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
variantsof 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
boolcanDeleteOutdated()
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
boolclassify()
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, orvariants_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: listexpectedSize()
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}|nullisRaster()
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
boollikePatterns()
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
booloutdatedFolders()
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|nullwithoutFolders()
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: arraywithoutOriginal()
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: listwithOverwrite()
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__construct()
private
__construct() : mixed
isOriginalEntry()
private
static isOriginalEntry(mixed $entry) : bool
Parameters
- $entry : mixed
-
item of
variants(from JSON, shape not guaranteed)
Return values
boolnumber()
private
static number(mixed $value) : int
Parameters
- $value : mixed
Return values
intprotectedNames()
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