Content checks: flag placeholder images instead of counting them as done

Files below fileadmin/placeholders/ (or named "...placeholder...") are grey
dummies. PageCheckService and ProductCheckService now report them:

- image cells carry `placeholder` = number of placeholder files (only when > 0);
  `ok` is unchanged (something is attached)
- summary.filled counts only cells without placeholders, new summary.placeholder
  counts the cells that have one
- detail images carry `placeholder: true|false`
- cards: `placeholder` = cards whose icon/image is a placeholder; record: the
  record image

Additive change to the JSON contract; frontend rendering follows (ticket).
Requested by Harry 2026-11-13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-13 18:47:12 +01:00
1 parent 20bcbeb731
commit 17b08df283
3 files changed
+135 -45

No files matched your search

@@ -0,0 +1,19 @@
#!/bin/sh
# Placeholder flag for the content checks (2026-11-13): permissions, syntax check,
# cache flush and SSR purge of the three check pages; "commit" as first argument
# commits the two services afterwards.
set -e
cd /usr/www/users/vitecevo/live
SRV=packages/vitec/Classes/Service
chmod 644 $SRV/PageCheckService.php $SRV/ProductCheckService.php migrations/pagecheck_placeholder_20261113.sh
php -l $SRV/PageCheckService.php
php -l $SRV/ProductCheckService.php
if [ "$1" = "commit" ]; then
git add $SRV/PageCheckService.php $SRV/ProductCheckService.php migrations/pagecheck_placeholder_20261113.sh
git commit -q -F migrations/pagecheck_placeholder_20261113.msg
git log --oneline -1
exit 0
fi
vendor/bin/typo3 cache:flush
vendor/bin/typo3 vitec:ssr-purge /markets/market-check /solutions/solution-check /products/product-check
vendor/bin/typo3 vitec:ssr-warm /markets/market-check /solutions/solution-check /products/product-check
@@ -30,10 +30,25 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* logos on a sub-market, insights on a main market) is delivered as * logos on a sub-market, insights on a main market) is delivered as
* {ok: null, count: 0} and counted neither as filled nor as applicable. * {ok: null, count: 0} and counted neither as filled nor as applicable.
* *
* Placeholders (Harry 2026-11-13): files below fileadmin/placeholders/ (or
* named "...placeholder...") are grey dummies that must not pass as finished.
* Image cells carry `placeholder` = number of such files when there is at
* least one; summary.filled counts only cells without placeholders, and
* summary.placeholder counts the cells that have one. Detail images carry
* `placeholder: true|false`. `ok` itself is unchanged (something is attached).
*
* Read fresh on every call, nothing cached (USER_INT in the renderer). * Read fresh on every call, nothing cached (USER_INT in the renderer).
*/ */
final class PageCheckService final class PageCheckService
{ {
/** SQL condition on sys_file f that marks a placeholder file. */
public const PLACEHOLDER_SQL = "(f.identifier LIKE '/placeholders/%' OR LOWER(f.name) LIKE '%placeholder%')";
public static function isPlaceholder(string $identifier, string $name): bool
{
return str_starts_with($identifier, '/placeholders/') || str_contains(strtolower($name), 'placeholder');
}
public const SCOPES = [ public const SCOPES = [
'markets' => [ 'markets' => [
'label' => 'Markets', 'label' => 'Markets',
@@ -137,9 +152,13 @@ final class PageCheckService
} }
} }
$elementFiles = $this->referenceCounts('tt_content', $allElementUids); $elementFiles = $this->referenceCounts('tt_content', $allElementUids);
$elementPlaceholders = $this->referenceCounts('tt_content', $allElementUids, true);
$pageFiles = $this->referenceCounts('pages', $pageUids); $pageFiles = $this->referenceCounts('pages', $pageUids);
$pagePlaceholders = $this->referenceCounts('pages', $pageUids, true);
$records = $this->records($cfg['recordTable'], $pageUids); $records = $this->records($cfg['recordTable'], $pageUids);
$recordFiles = $this->referenceCounts($cfg['recordTable'], array_map(static fn(array $r): int => (int)$r['uid'], $records)); $recordUids = array_map(static fn(array $r): int => (int)$r['uid'], $records);
$recordFiles = $this->referenceCounts($cfg['recordTable'], $recordUids);
$recordPlaceholders = $this->referenceCounts($cfg['recordTable'], $recordUids, true);
// Empty selection = what the testimonial band shows: quoted stories, // Empty selection = what the testimonial band shows: quoted stories,
// at most twelve (UsecaseListJsonRenderer::testimonialRows). // at most twelve (UsecaseListJsonRenderer::testimonialRows).
$allStories = min(12, $this->countAll('tx_vitec_domain_model_usecase', "deleted = 0 AND hidden = 0 AND hideonwebsite = 0 AND TRIM(COALESCE(quote, '')) <> ''")); $allStories = min(12, $this->countAll('tx_vitec_domain_model_usecase', "deleted = 0 AND hidden = 0 AND hideonwebsite = 0 AND TRIM(COALESCE(quote, '')) <> ''"));
@@ -147,6 +166,7 @@ final class PageCheckService
$keys = array_column(self::FIELDS, 'key'); $keys = array_column(self::FIELDS, 'key');
$filled = array_fill_keys($keys, 0); $filled = array_fill_keys($keys, 0);
$placeholder = array_fill_keys($keys, 0);
$applicable = array_fill_keys($keys, 0); $applicable = array_fill_keys($keys, 0);
$byType = []; $byType = [];
$rows = []; $rows = [];
@@ -158,7 +178,18 @@ final class PageCheckService
$visible = array_values(array_filter($els, static fn(array $e): bool => (int)$e['hidden'] === 0)); $visible = array_values(array_filter($els, static fn(array $e): bool => (int)$e['hidden'] === 0));
$hiddenCount = count($els) - count($visible); $hiddenCount = count($els) - count($visible);
$record = $records[$uid] ?? null; $record = $records[$uid] ?? null;
$raw = $this->pageChecks($type, $visible, $elementFiles, $pageFiles[$uid] ?? [], $p, $record, $record ? ($recordFiles[(int)$record['uid']] ?? []) : [], $allStories, $allCustomers); $recordUid = $record ? (int)$record['uid'] : 0;
$raw = $this->pageChecks(
$type,
$visible,
['n' => $elementFiles, 'ph' => $elementPlaceholders],
['n' => $pageFiles[$uid] ?? [], 'ph' => $pagePlaceholders[$uid] ?? []],
$p,
$record,
['n' => $recordFiles[$recordUid] ?? [], 'ph' => $recordPlaceholders[$recordUid] ?? []],
$allStories,
$allCustomers
);
$checks = []; $checks = [];
foreach (self::FIELDS as $field) { foreach (self::FIELDS as $field) {
@@ -169,7 +200,9 @@ final class PageCheckService
} }
$checks[$key] = $raw[$key]; $checks[$key] = $raw[$key];
$applicable[$key]++; $applicable[$key]++;
if ($raw[$key]['ok'] === true) { if (($raw[$key]['placeholder'] ?? 0) > 0) {
$placeholder[$key]++;
} elseif ($raw[$key]['ok'] === true) {
$filled[$key]++; $filled[$key]++;
} }
} }
@@ -181,7 +214,7 @@ final class PageCheckService
'scopeLabel' => $cfg['label'], 'scopeLabel' => $cfg['label'],
'generatedAt' => date(\DATE_ATOM), 'generatedAt' => date(\DATE_ATOM),
'fields' => self::FIELDS, 'fields' => self::FIELDS,
'summary' => ['pages' => count($rows), 'byType' => $byType, 'filled' => $filled, 'applicable' => $applicable], 'summary' => ['pages' => count($rows), 'byType' => $byType, 'filled' => $filled, 'placeholder' => $placeholder, 'applicable' => $applicable],
'pages' => $rows, 'pages' => $rows,
]; ];
} }
@@ -275,17 +308,19 @@ final class PageCheckService
/** /**
* @param array<int, array<string, mixed>> $visible visible elements in page order * @param array<int, array<string, mixed>> $visible visible elements in page order
* @param array<int, array<string, int>> $elementFiles [elementUid][fieldname] = count * @param array{n: array<int, array<string, int>>, ph: array<int, array<string, int>>} $elementFiles n/ph: [elementUid][fieldname] = count of all / of placeholder files
* @param array<string, int> $pageFiles [fieldname] = count * @param array{n: array<string, int>, ph: array<string, int>} $pageFiles [fieldname] = count
* @param array<string, mixed> $page * @param array<string, mixed> $page
* @param array<string, mixed>|null $record * @param array<string, mixed>|null $record
* @param array<string, int> $recordFiles * @param array{n: array<string, int>, ph: array<string, int>} $recordFiles
* @return array<string, array{ok: bool, count: int, icons?: int}> * @return array<string, array{ok: bool, count: int, icons?: int, placeholder?: int}>
*/ */
private function pageChecks(string $type, array $visible, array $elementFiles, array $pageFiles, array $page, ?array $record, array $recordFiles, int $allStories, int $allCustomers): array private function pageChecks(string $type, array $visible, array $elementFiles, array $pageFiles, array $page, ?array $record, array $recordFiles, int $allStories, int $allCustomers): array
{ {
$hero = $this->firstOf($visible, 'vitec_markethero') ?? $this->firstOf($visible, 'vitec_herosection'); $hero = $this->firstOf($visible, 'vitec_markethero') ?? $this->firstOf($visible, 'vitec_herosection');
$heroFiles = $hero ? ($elementFiles[(int)$hero['uid']] ?? []) : []; $heroFiles = $hero ? ($elementFiles['n'][(int)$hero['uid']] ?? []) : [];
$heroPh = $hero ? ($elementFiles['ph'][(int)$hero['uid']] ?? []) : [];
$heroFields = ['vitec_bgimage', 'vitec_hero_bgimage', 'vitec_hero_image'];
$lead = null; $lead = null;
foreach ($visible as $el) { foreach ($visible as $el) {
if ($el['CType'] === 'vitec_introparagraph' && in_array((int)$el['colPos'], self::HERO_COLPOS, true)) { if ($el['CType'] === 'vitec_introparagraph' && in_array((int)$el['colPos'], self::HERO_COLPOS, true)) {
@@ -294,15 +329,21 @@ final class PageCheckService
} }
} }
$intro = $this->firstOf($visible, 'vitec_marketintro'); $intro = $this->firstOf($visible, 'vitec_marketintro');
$introFiles = $intro ? ($elementFiles[(int)$intro['uid']] ?? []) : []; $introFiles = $intro ? ($elementFiles['n'][(int)$intro['uid']] ?? []) : [];
$introPh = $intro ? ($elementFiles['ph'][(int)$intro['uid']] ?? []) : [];
$cards = array_values(array_filter($visible, static fn(array $e): bool => $e['CType'] === 'vitec_card')); $cards = array_values(array_filter($visible, static fn(array $e): bool => $e['CType'] === 'vitec_card'));
$cardVisuals = 0; $cardVisuals = 0;
$cardPlaceholders = 0;
foreach ($cards as $c) { foreach ($cards as $c) {
$f = $elementFiles[(int)$c['uid']] ?? []; $f = $elementFiles['n'][(int)$c['uid']] ?? [];
$ph = $elementFiles['ph'][(int)$c['uid']] ?? [];
if (($f['vitec_icon'] ?? 0) > 0 || ($f['vitec_image'] ?? 0) > 0) { if (($f['vitec_icon'] ?? 0) > 0 || ($f['vitec_image'] ?? 0) > 0) {
$cardVisuals++; $cardVisuals++;
} }
if (($ph['vitec_icon'] ?? 0) > 0 || ($ph['vitec_image'] ?? 0) > 0) {
$cardPlaceholders++;
}
} }
$stories = $this->firstOf($visible, 'vitec_usecaselist'); $stories = $this->firstOf($visible, 'vitec_usecaselist');
@@ -327,7 +368,8 @@ final class PageCheckService
} }
$insights = $this->firstOf($visible, 'vitec_marketinsights'); $insights = $this->firstOf($visible, 'vitec_marketinsights');
$insightsFiles = $insights ? ($elementFiles[(int)$insights['uid']] ?? []) : []; $insightsFiles = $insights ? ($elementFiles['n'][(int)$insights['uid']] ?? []) : [];
$insightsPh = $insights ? ($elementFiles['ph'][(int)$insights['uid']] ?? []) : [];
$logos = $this->firstOf($visible, 'vitec_customerlogos'); $logos = $this->firstOf($visible, 'vitec_customerlogos');
$logoCount = 0; $logoCount = 0;
@@ -341,30 +383,33 @@ final class PageCheckService
$recordFilled = 0; $recordFilled = 0;
if ($record !== null) { if ($record !== null) {
$recordFilled += (($recordFiles['image'] ?? 0) > 0) ? 1 : 0; $recordFilled += (($recordFiles['n']['image'] ?? 0) > 0) ? 1 : 0;
$recordFilled += $this->filled($record['teaser'] ?? '') ? 1 : 0; $recordFilled += $this->filled($record['teaser'] ?? '') ? 1 : 0;
$recordFilled += $this->filled($record['description'] ?? '') ? 1 : 0; $recordFilled += $this->filled($record['description'] ?? '') ? 1 : 0;
} }
$sum = static fn(array $counts, array $fields): int => array_sum(array_map(static fn(string $f): int => (int)($counts[$f] ?? 0), $fields));
return [ return [
'hero' => $this->check(($heroFiles['vitec_bgimage'] ?? 0) + ($heroFiles['vitec_hero_bgimage'] ?? 0) + ($heroFiles['vitec_hero_image'] ?? 0)), 'hero' => $this->check($sum($heroFiles, $heroFields), $sum($heroPh, $heroFields)),
'subline' => $this->check($hero && ($this->filled($hero['vitec_subline'] ?? '') || $this->filled($hero['subheader'] ?? '')) ? 1 : 0), 'subline' => $this->check($hero && ($this->filled($hero['vitec_subline'] ?? '') || $this->filled($hero['subheader'] ?? '')) ? 1 : 0),
'lead' => $this->check($lead && $this->filled($lead['bodytext'] ?? '') ? 1 : 0), 'lead' => $this->check($lead && $this->filled($lead['bodytext'] ?? '') ? 1 : 0),
'intro' => $this->check($intro && $this->filled($intro['vitec_intro_text'] ?? '') ? 1 : 0), 'intro' => $this->check($intro && $this->filled($intro['vitec_intro_text'] ?? '') ? 1 : 0),
'introImage' => $this->check($introFiles['vitec_image'] ?? 0), 'introImage' => $this->check($introFiles['vitec_image'] ?? 0, $introPh['vitec_image'] ?? 0),
'introCta' => $this->check($intro && $this->filled($intro['vitec_learnmore_label'] ?? '') && $this->filled($intro['vitec_learnmore_target'] ?? '') ? 1 : 0), 'introCta' => $this->check($intro && $this->filled($intro['vitec_learnmore_label'] ?? '') && $this->filled($intro['vitec_learnmore_target'] ?? '') ? 1 : 0),
'cards' => $this->check(count($cards)) + ['icons' => $cardVisuals], 'cards' => $this->check(count($cards), $cardPlaceholders) + ['icons' => $cardVisuals],
'stories' => ['ok' => $stories !== null, 'count' => $storyCount], 'stories' => ['ok' => $stories !== null, 'count' => $storyCount],
'products' => ['ok' => $hasProducts, 'count' => $productCount], 'products' => ['ok' => $hasProducts, 'count' => $productCount],
'insights' => $this->check($insights && $this->filled($insights['vitec_text1'] ?? '') ? 1 : 0), 'insights' => $this->check($insights && $this->filled($insights['vitec_text1'] ?? '') ? 1 : 0),
'insightsCover' => $this->check($insightsFiles['vitec_video_cover'] ?? 0), 'insightsCover' => $this->check($insightsFiles['vitec_video_cover'] ?? 0, $insightsPh['vitec_video_cover'] ?? 0),
'insightsCta' => $this->check($insights && $this->filled($insights['vitec_cta_label'] ?? '') && $this->filled($insights['vitec_cta_target'] ?? '') ? 1 : 0), 'insightsCta' => $this->check($insights && $this->filled($insights['vitec_cta_label'] ?? '') && $this->filled($insights['vitec_cta_target'] ?? '') ? 1 : 0),
'logos' => ['ok' => $logos !== null, 'count' => $logoCount], 'logos' => ['ok' => $logos !== null, 'count' => $logoCount],
'cta' => $this->check($cta && $this->filled($cta['vitec_primary_cta_label'] ?? '') ? 1 : 0), 'cta' => $this->check($cta && $this->filled($cta['vitec_primary_cta_label'] ?? '') ? 1 : 0),
'seo' => $this->check($this->filled($page['description'] ?? '') ? 1 : 0), 'seo' => $this->check($this->filled($page['description'] ?? '') ? 1 : 0),
'og' => $this->check($pageFiles['og_image'] ?? 0), 'og' => $this->check($pageFiles['n']['og_image'] ?? 0, $pageFiles['ph']['og_image'] ?? 0),
'media' => $this->check($pageFiles['media'] ?? 0), 'media' => $this->check($pageFiles['n']['media'] ?? 0, $pageFiles['ph']['media'] ?? 0),
'record' => ['ok' => $record !== null && ($recordFiles['image'] ?? 0) > 0 && $this->filled($record['teaser'] ?? ''), 'count' => $recordFilled], 'record' => ['ok' => $record !== null && ($recordFiles['n']['image'] ?? 0) > 0 && $this->filled($record['teaser'] ?? ''), 'count' => $recordFilled]
+ ((($recordFiles['ph']['image'] ?? 0) > 0) ? ['placeholder' => (int)$recordFiles['ph']['image']] : []),
]; ];
} }
@@ -404,10 +449,13 @@ final class PageCheckService
]; ];
} }
/** @return array{ok: bool, count: int} */ /**
private function check(int $count): array * @param int $placeholder number of placeholder files among $count; emitted only when > 0
* @return array{ok: bool, count: int, placeholder?: int}
*/
private function check(int $count, int $placeholder = 0): array
{ {
return ['ok' => $count > 0, 'count' => $count]; return ['ok' => $count > 0, 'count' => $count] + ($placeholder > 0 ? ['placeholder' => $placeholder] : []);
} }
private function filled(mixed $value): bool private function filled(mixed $value): bool
@@ -526,17 +574,20 @@ final class PageCheckService
* @param int[] $uids * @param int[] $uids
* @return array<int, array<string, int>> [uid][fieldname] = count * @return array<int, array<string, int>> [uid][fieldname] = count
*/ */
private function referenceCounts(string $table, array $uids): array private function referenceCounts(string $table, array $uids, bool $placeholdersOnly = false): array
{ {
if ($uids === []) { if ($uids === []) {
return []; return [];
} }
$rows = $this->connection('sys_file_reference')->fetchAllAssociative( $in = implode(',', array_map('intval', $uids));
'SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference $sql = $placeholdersOnly
WHERE deleted = 0 AND hidden = 0 AND tablenames = ? AND uid_foreign IN (' . implode(',', array_map('intval', $uids)) . ') ? 'SELECT r.uid_foreign, r.fieldname, COUNT(*) AS n FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local
GROUP BY uid_foreign, fieldname', WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = ? AND r.uid_foreign IN (' . $in . ') AND ' . self::PLACEHOLDER_SQL . '
[$table] GROUP BY r.uid_foreign, r.fieldname'
); : 'SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
WHERE deleted = 0 AND hidden = 0 AND tablenames = ? AND uid_foreign IN (' . $in . ')
GROUP BY uid_foreign, fieldname';
$rows = $this->connection('sys_file_reference')->fetchAllAssociative($sql, [$table]);
$out = []; $out = [];
foreach ($rows as $r) { foreach ($rows as $r) {
$out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n']; $out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n'];
@@ -548,7 +599,7 @@ final class PageCheckService
* Files of one record with original URL and a ~480 px thumbnail. * Files of one record with original URL and a ~480 px thumbnail.
* *
* @param string[]|null $fieldnames null = all fields * @param string[]|null $fieldnames null = all fields
* @return array<int, array{field: string, url: string, thumb: string, name: string, mime: string}> * @return array<int, array{field: string, url: string, thumb: string, name: string, mime: string, placeholder: bool}>
*/ */
private function files(string $table, int $uid, ?array $fieldnames = null): array private function files(string $table, int $uid, ?array $fieldnames = null): array
{ {
@@ -570,6 +621,7 @@ final class PageCheckService
'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url), 'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url),
'name' => (string)$r['name'], 'name' => (string)$r['name'],
'mime' => (string)$r['mime_type'], 'mime' => (string)$r['mime_type'],
'placeholder' => self::isPlaceholder((string)$r['identifier'], (string)$r['name']),
]; ];
} }
return $out; return $out;
@@ -24,6 +24,13 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* detail(): one product with all texts (RTE HTML as stored), images * detail(): one product with all texts (RTE HTML as stored), images
* (original URL + ~480 px thumbnail), videos, feature cards * (original URL + ~480 px thumbnail), videos, feature cards
* with icons, resources, datasheets, solutions. * with icons, resources, datasheets, solutions.
*
* Placeholders (Harry 2026-11-13, same rule as PageCheckService): files below
* fileadmin/placeholders/ or named "...placeholder..." do not pass as
* finished. Image cells carry `placeholder` = number of such files (only when
* > 0), summary.filled counts only cells without placeholders and
* summary.placeholder the cells that have one; detail images carry
* `placeholder: true|false`.
*/ */
final class ProductCheckService final class ProductCheckService
{ {
@@ -84,20 +91,23 @@ final class ProductCheckService
$products = $this->products(); $products = $this->products();
$uids = array_map(static fn(array $p): int => (int)$p['uid'], $products); $uids = array_map(static fn(array $p): int => (int)$p['uid'], $products);
$files = $this->fileCounts($uids); $files = $this->fileCounts($uids);
$placeholders = $this->fileCounts($uids, true);
$features = $this->featureCounts($uids); $features = $this->featureCounts($uids);
$resources = $this->countBy('tx_vitec_product_resource', 'product', 'deleted = 0 AND hidden = 0'); $resources = $this->countBy('tx_vitec_product_resource', 'product', 'deleted = 0 AND hidden = 0');
$datasheets = $this->mmCounts('tx_vitec_product_datasheet_mm', 'tx_vitec_domain_model_download'); $datasheets = $this->mmCounts('tx_vitec_product_datasheet_mm', 'tx_vitec_domain_model_download');
$solutions = $this->mmCounts('tx_vitec_product_solution_mm', 'tx_vitec_domain_model_solution'); $solutions = $this->mmCounts('tx_vitec_product_solution_mm', 'tx_vitec_domain_model_solution');
$filled = array_fill_keys(array_column(self::FIELDS, 'key'), 0); $filled = array_fill_keys(array_column(self::FIELDS, 'key'), 0);
$placeholder = array_fill_keys(array_column(self::FIELDS, 'key'), 0);
$rows = []; $rows = [];
foreach ($products as $p) { foreach ($products as $p) {
$uid = (int)$p['uid']; $uid = (int)$p['uid'];
$f = $files[$uid] ?? []; $f = $files[$uid] ?? [];
$ph = $placeholders[$uid] ?? [];
$checks = [ $checks = [
'hero' => $this->check($f['heroimage'] ?? 0), 'hero' => $this->check($f['heroimage'] ?? 0, $ph['heroimage'] ?? 0),
'photos' => $this->check($f['productimage'] ?? 0), 'photos' => $this->check($f['productimage'] ?? 0, $ph['productimage'] ?? 0),
'og' => $this->check($f['ogimage'] ?? 0), 'og' => $this->check($f['ogimage'] ?? 0, $ph['ogimage'] ?? 0),
'video' => $this->check($f['videofile'] ?? 0), 'video' => $this->check($f['videofile'] ?? 0),
'teaser' => $this->check($this->filled($p['teaser']) ? 1 : 0), 'teaser' => $this->check($this->filled($p['teaser']) ? 1 : 0),
'subtitle' => $this->check($this->filled($p['subtitle']) ? 1 : 0), 'subtitle' => $this->check($this->filled($p['subtitle']) ? 1 : 0),
@@ -112,7 +122,9 @@ final class ProductCheckService
'solutions' => $this->check($solutions[$uid] ?? 0), 'solutions' => $this->check($solutions[$uid] ?? 0),
]; ];
foreach ($checks as $key => $c) { foreach ($checks as $key => $c) {
if ($c['ok']) { if (($c['placeholder'] ?? 0) > 0) {
$placeholder[$key]++;
} elseif ($c['ok']) {
$filled[$key]++; $filled[$key]++;
} }
} }
@@ -122,7 +134,7 @@ final class ProductCheckService
return [ return [
'generatedAt' => date(\DATE_ATOM), 'generatedAt' => date(\DATE_ATOM),
'fields' => self::FIELDS, 'fields' => self::FIELDS,
'summary' => ['products' => count($rows), 'filled' => $filled], 'summary' => ['products' => count($rows), 'filled' => $filled, 'placeholder' => $placeholder],
'products' => $rows, 'products' => $rows,
]; ];
} }
@@ -191,9 +203,9 @@ final class ProductCheckService
} }
/** @return array{ok: bool, count: int} */ /** @return array{ok: bool, count: int} */
private function check(int $count): array private function check(int $count, int $placeholder = 0): array
{ {
return ['ok' => $count > 0, 'count' => $count]; return ['ok' => $count > 0, 'count' => $count] + ($placeholder > 0 ? ['placeholder' => $placeholder] : []);
} }
private function filled(mixed $value): bool private function filled(mixed $value): bool
@@ -218,18 +230,24 @@ final class ProductCheckService
* @param int[] $uids * @param int[] $uids
* @return array<int, array<string, int>> [productUid][fieldname] = count * @return array<int, array<string, int>> [productUid][fieldname] = count
*/ */
private function fileCounts(array $uids): array private function fileCounts(array $uids, bool $placeholdersOnly = false): array
{ {
if ($uids === []) { if ($uids === []) {
return []; return [];
} }
$rows = $this->connection()->executeQuery( $in = implode(',', array_map('intval', $uids));
"SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference $sql = $placeholdersOnly
? "SELECT r.uid_foreign, r.fieldname, COUNT(*) AS n FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_domain_model_product'
AND r.fieldname IN ('heroimage','productimage','ogimage','videofile')
AND r.uid_foreign IN (" . $in . ') AND ' . PageCheckService::PLACEHOLDER_SQL . '
GROUP BY r.uid_foreign, r.fieldname'
: "SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tx_vitec_domain_model_product' WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tx_vitec_domain_model_product'
AND fieldname IN ('heroimage','productimage','ogimage','videofile') AND fieldname IN ('heroimage','productimage','ogimage','videofile')
AND uid_foreign IN (" . implode(',', array_map('intval', $uids)) . ') AND uid_foreign IN (" . $in . ')
GROUP BY uid_foreign, fieldname' GROUP BY uid_foreign, fieldname';
)->fetchAllAssociative(); $rows = $this->connection()->executeQuery($sql)->fetchAllAssociative();
$out = []; $out = [];
foreach ($rows as $r) { foreach ($rows as $r) {
$out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n']; $out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n'];
@@ -241,7 +259,7 @@ final class ProductCheckService
* Files of one product with original URL and a ~480 px thumbnail * Files of one product with original URL and a ~480 px thumbnail
* (SVG and non-images keep the original as thumbnail). * (SVG and non-images keep the original as thumbnail).
* *
* @return array<string, array<int, array{url: string, thumb: string, name: string, mime: string}>> * @return array<string, array<int, array{url: string, thumb: string, name: string, mime: string, placeholder: bool}>>
*/ */
private function files(int $uid): array private function files(int $uid): array
{ {
@@ -261,6 +279,7 @@ final class ProductCheckService
'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url), 'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url),
'name' => (string)$r['name'], 'name' => (string)$r['name'],
'mime' => (string)$r['mime_type'], 'mime' => (string)$r['mime_type'],
'placeholder' => PageCheckService::isPlaceholder((string)$r['identifier'], (string)$r['name']),
]; ];
} }
return $out; return $out;