Checkpoint: headless JSON architecture documented + cleanup (Stufe 1+2)
Restore point before Stufe 3 (central resolver-service refactoring). Includes this session's work: - Success Story (usecase) rebuild: new fields/tabs, UsecaseSerializer, thin List/Show renderers, MM relations, inline content elements. - vitec_columns content block (two-column layout with per-item content) incl. unified header section; special-cased JSON resolution. - Container per-column flex (items[].config align/justify) + gap on parent. - Unified header section across CEs/plugins/containers; header fields in JSON. - ISO-style architecture spec: Documentation/Headless-JSON-Architecture.md. - Cleanup: removed local scratch + verified .bak backups. - Fixes: B-6 (undefined thumbnail variable in Downloadcardcollection image resolver), B-7 (unsatisfiable legacy CType OR branch in Downloadcard/Datasheets). Rollback: git reset --hard snapshot-2026-07-09-pre-stufe3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6759186cb
commit
bb7c03235d
237 files changed
+6698
-1749
No files matched your search
Executable → Regular
+185
@@ -93,6 +93,24 @@ final class PageJsonLdRenderer
|
||||
$hasOrganization
|
||||
);
|
||||
}
|
||||
// FAQPage when the page contains at least one vitec_faq element.
|
||||
$faqItems = $this->collectFaqItems($pageId);
|
||||
if ($faqItems !== []) {
|
||||
$nodes[] = $service->buildFaqPage($faqItems, $base);
|
||||
}
|
||||
// Event nodes when the page contains a vitec_eventlist element.
|
||||
foreach ($this->collectUpcomingEvents($pageId) as $eventRow) {
|
||||
$nodes[] = $service->buildEvent(
|
||||
$eventRow,
|
||||
$base,
|
||||
$this->eventImageUrls((int)$eventRow['uid'], $base, $service),
|
||||
$hasOrganization
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return $service->encodeGraph($nodes);
|
||||
} catch (\Throwable $e) {
|
||||
@@ -225,4 +243,171 @@ final class PageJsonLdRenderer
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect question/answer pairs from all visible vitec_faq elements on
|
||||
* this page (Content Blocks collection child table).
|
||||
*
|
||||
* @return list<array{question:string,answer:string}>
|
||||
*/
|
||||
private function collectFaqItems(int $pageId): array
|
||||
{
|
||||
try {
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
$elements = $qb
|
||||
->select('uid')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_faq', ParameterType::STRING)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if ($elements === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($elements as $element) {
|
||||
$childQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('vitec_faq_items');
|
||||
$rows = $childQb
|
||||
->select('*')
|
||||
->from('vitec_faq_items')
|
||||
->where(
|
||||
$childQb->expr()->eq('foreign_table_parent_uid', $childQb->createNamedParameter((int)$element['uid'], ParameterType::INTEGER)),
|
||||
$childQb->expr()->eq('deleted', 0),
|
||||
$childQb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->orderBy('sorting', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$items[] = [
|
||||
'question' => (string)($row['question'] ?? $row['vitec_question'] ?? ''),
|
||||
'answer' => (string)($row['answer'] ?? $row['vitec_answer'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
} catch (\Throwable $e) {
|
||||
// Table may not exist yet (schema not updated) — skip silently.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Upcoming event rows when this page contains at least one visible
|
||||
* vitec_eventlist element. Empty array otherwise.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
private function collectUpcomingEvents(int $pageId): array
|
||||
{
|
||||
try {
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
$eventlistElement = $qb
|
||||
->select('uid', 'pi_flexform')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_eventlist', ParameterType::STRING)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
if (!$eventlistElement) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Mirror the plugin's "days in advance" window so structured data
|
||||
// matches what the page actually shows.
|
||||
$daysInAdvance = 0;
|
||||
$flexFormService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\FlexFormService::class);
|
||||
$flexSettings = $flexFormService->convertFlexFormContentToArray((string)($eventlistElement['pi_flexform'] ?? ''))['settings'] ?? [];
|
||||
$daysInAdvance = (int)($flexSettings['daysinadvance'] ?? 0);
|
||||
|
||||
$todayMidnight = strtotime('today');
|
||||
$eventQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_domain_model_event');
|
||||
$expr = $eventQb->expr();
|
||||
|
||||
$eventQb
|
||||
->select('*')
|
||||
->from('tx_vitec_domain_model_event')
|
||||
->where(
|
||||
$expr->eq('deleted', 0),
|
||||
$expr->eq('hidden', 0),
|
||||
$expr->eq('hideonwebsite', 0),
|
||||
$expr->or(
|
||||
$expr->gte('eventend', $eventQb->createNamedParameter($todayMidnight, ParameterType::INTEGER)),
|
||||
$expr->and(
|
||||
$expr->eq('eventend', 0),
|
||||
$expr->gte('eventstart', $eventQb->createNamedParameter($todayMidnight, ParameterType::INTEGER))
|
||||
)
|
||||
)
|
||||
)
|
||||
->orderBy('eventstart', 'ASC');
|
||||
|
||||
if ($daysInAdvance > 0) {
|
||||
$eventQb->andWhere(
|
||||
$expr->lt('eventstart', $eventQb->createNamedParameter($todayMidnight + ($daysInAdvance * 86400), ParameterType::INTEGER))
|
||||
);
|
||||
}
|
||||
|
||||
return $eventQb->executeQuery()->fetchAllAssociative();
|
||||
} catch (\Throwable $e) {
|
||||
// Table may not exist yet — skip silently.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Root-relative logo URLs for an event (fieldname=image).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function eventImageUrls(int $eventUid, string $base, $service): array
|
||||
{
|
||||
try {
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_file_reference');
|
||||
$rows = $qb
|
||||
->select('f.identifier')
|
||||
->from('sys_file_reference', 'fr')
|
||||
->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid')
|
||||
->where(
|
||||
$qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_event', ParameterType::STRING)),
|
||||
$qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('image', ParameterType::STRING)),
|
||||
$qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($eventUid, ParameterType::INTEGER)),
|
||||
$qb->expr()->eq('fr.deleted', 0),
|
||||
$qb->expr()->eq('f.missing', 0)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$urls = [];
|
||||
foreach ($rows as $row) {
|
||||
$identifier = (string)($row['identifier'] ?? '');
|
||||
if ($identifier !== '') {
|
||||
$urls[] = '/fileadmin' . $identifier;
|
||||
}
|
||||
}
|
||||
return $urls;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user