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:
1 parent
20bcbeb731
commit
17b08df283
3 files changed
+137
-47
No files matched your search
@@ -30,10 +30,25 @@ use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
* 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.
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
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 = [
|
||||
'markets' => [
|
||||
'label' => 'Markets',
|
||||
@@ -137,9 +152,13 @@ final class PageCheckService
|
||||
}
|
||||
}
|
||||
$elementFiles = $this->referenceCounts('tt_content', $allElementUids);
|
||||
$elementPlaceholders = $this->referenceCounts('tt_content', $allElementUids, true);
|
||||
$pageFiles = $this->referenceCounts('pages', $pageUids);
|
||||
$pagePlaceholders = $this->referenceCounts('pages', $pageUids, true);
|
||||
$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,
|
||||
// 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, '')) <> ''"));
|
||||
@@ -147,6 +166,7 @@ final class PageCheckService
|
||||
|
||||
$keys = array_column(self::FIELDS, 'key');
|
||||
$filled = array_fill_keys($keys, 0);
|
||||
$placeholder = array_fill_keys($keys, 0);
|
||||
$applicable = array_fill_keys($keys, 0);
|
||||
$byType = [];
|
||||
$rows = [];
|
||||
@@ -158,7 +178,18 @@ final class PageCheckService
|
||||
$visible = array_values(array_filter($els, static fn(array $e): bool => (int)$e['hidden'] === 0));
|
||||
$hiddenCount = count($els) - count($visible);
|
||||
$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 = [];
|
||||
foreach (self::FIELDS as $field) {
|
||||
@@ -169,7 +200,9 @@ final class PageCheckService
|
||||
}
|
||||
$checks[$key] = $raw[$key];
|
||||
$applicable[$key]++;
|
||||
if ($raw[$key]['ok'] === true) {
|
||||
if (($raw[$key]['placeholder'] ?? 0) > 0) {
|
||||
$placeholder[$key]++;
|
||||
} elseif ($raw[$key]['ok'] === true) {
|
||||
$filled[$key]++;
|
||||
}
|
||||
}
|
||||
@@ -181,7 +214,7 @@ final class PageCheckService
|
||||
'scopeLabel' => $cfg['label'],
|
||||
'generatedAt' => date(\DATE_ATOM),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
@@ -275,17 +308,19 @@ final class PageCheckService
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $visible visible elements in page order
|
||||
* @param array<int, array<string, int>> $elementFiles [elementUid][fieldname] = count
|
||||
* @param array<string, int> $pageFiles [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{n: array<string, int>, ph: array<string, int>} $pageFiles [fieldname] = count
|
||||
* @param array<string, mixed> $page
|
||||
* @param array<string, mixed>|null $record
|
||||
* @param array<string, int> $recordFiles
|
||||
* @return array<string, array{ok: bool, count: int, icons?: int}>
|
||||
* @param array{n: array<string, int>, ph: array<string, int>} $recordFiles
|
||||
* @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
|
||||
{
|
||||
$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;
|
||||
foreach ($visible as $el) {
|
||||
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');
|
||||
$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'));
|
||||
$cardVisuals = 0;
|
||||
$cardPlaceholders = 0;
|
||||
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) {
|
||||
$cardVisuals++;
|
||||
}
|
||||
if (($ph['vitec_icon'] ?? 0) > 0 || ($ph['vitec_image'] ?? 0) > 0) {
|
||||
$cardPlaceholders++;
|
||||
}
|
||||
}
|
||||
|
||||
$stories = $this->firstOf($visible, 'vitec_usecaselist');
|
||||
@@ -327,7 +368,8 @@ final class PageCheckService
|
||||
}
|
||||
|
||||
$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');
|
||||
$logoCount = 0;
|
||||
@@ -341,30 +383,33 @@ final class PageCheckService
|
||||
|
||||
$recordFilled = 0;
|
||||
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['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 [
|
||||
'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),
|
||||
'lead' => $this->check($lead && $this->filled($lead['bodytext'] ?? '') ? 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),
|
||||
'cards' => $this->check(count($cards)) + ['icons' => $cardVisuals],
|
||||
'cards' => $this->check(count($cards), $cardPlaceholders) + ['icons' => $cardVisuals],
|
||||
'stories' => ['ok' => $stories !== null, 'count' => $storyCount],
|
||||
'products' => ['ok' => $hasProducts, 'count' => $productCount],
|
||||
'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),
|
||||
'logos' => ['ok' => $logos !== null, 'count' => $logoCount],
|
||||
'cta' => $this->check($cta && $this->filled($cta['vitec_primary_cta_label'] ?? '') ? 1 : 0),
|
||||
'seo' => $this->check($this->filled($page['description'] ?? '') ? 1 : 0),
|
||||
'og' => $this->check($pageFiles['og_image'] ?? 0),
|
||||
'media' => $this->check($pageFiles['media'] ?? 0),
|
||||
'record' => ['ok' => $record !== null && ($recordFiles['image'] ?? 0) > 0 && $this->filled($record['teaser'] ?? ''), 'count' => $recordFilled],
|
||||
'og' => $this->check($pageFiles['n']['og_image'] ?? 0, $pageFiles['ph']['og_image'] ?? 0),
|
||||
'media' => $this->check($pageFiles['n']['media'] ?? 0, $pageFiles['ph']['media'] ?? 0),
|
||||
'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
|
||||
@@ -526,17 +574,20 @@ final class PageCheckService
|
||||
* @param int[] $uids
|
||||
* @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 === []) {
|
||||
return [];
|
||||
}
|
||||
$rows = $this->connection('sys_file_reference')->fetchAllAssociative(
|
||||
'SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
|
||||
WHERE deleted = 0 AND hidden = 0 AND tablenames = ? AND uid_foreign IN (' . implode(',', array_map('intval', $uids)) . ')
|
||||
GROUP BY uid_foreign, fieldname',
|
||||
[$table]
|
||||
);
|
||||
$in = implode(',', array_map('intval', $uids));
|
||||
$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 = ? AND r.uid_foreign IN (' . $in . ') AND ' . self::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 = ? AND uid_foreign IN (' . $in . ')
|
||||
GROUP BY uid_foreign, fieldname';
|
||||
$rows = $this->connection('sys_file_reference')->fetchAllAssociative($sql, [$table]);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$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.
|
||||
*
|
||||
* @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
|
||||
{
|
||||
@@ -570,6 +621,7 @@ final class PageCheckService
|
||||
'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url),
|
||||
'name' => (string)$r['name'],
|
||||
'mime' => (string)$r['mime_type'],
|
||||
'placeholder' => self::isPlaceholder((string)$r['identifier'], (string)$r['name']),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
|
||||
Reference in new issue
Block a user