[ProductListJsonRenderer::class, 'products'], 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], 'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'], 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], 'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'], 'vitec_eventlist' => [EventlistJsonRenderer::class, 'eventlist'], 'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'], 'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'], 'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'], 'vitec_contactform' => [FormsJsonRenderer::class, 'form'], 'vitec_demoform' => [FormsJsonRenderer::class, 'form'], 'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'], 'news_pi1' => [NewsJsonRenderer::class, 'news'], 'news_newsliststicky' => [NewsJsonRenderer::class, 'news'], 'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'], 'news_newsdetail' => [NewsJsonRenderer::class, 'news'], 'news_newsdatemenu' => [NewsJsonRenderer::class, 'news'], 'news_categorylist' => [NewsJsonRenderer::class, 'news'], 'news_newssearchform' => [NewsJsonRenderer::class, 'news'], 'news_newssearchresult' => [NewsJsonRenderer::class, 'news'], 'news_taglist' => [NewsJsonRenderer::class, 'news'], ]; /** * Resolve a typolink string to a normalised tt_content JSON element. * * Accepted forms (all may include an optional anchor fragment): * - "t3://record?identifier=tt_content&uid=N" * - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page; * the fragment is the tt_content uid) * - "" (legacy: bare tt_content uid) * - everything else → null (pure page links etc.) * * @return array|null */ public static function resolveLink(?string $link): ?array { try { $link = trim((string)$link); if ($link === '') { return null; } $uid = self::extractTtContentUid($link); if ($uid <= 0) { return null; } $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); $row = $qb ->select('*') ->from('tt_content') ->where( $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAssociative(); if (!$row) { return null; } return self::normaliseRecord($row); } catch (\Throwable $e) { return null; } } /** * Normalise a tt_content DB row to the same envelope shape that * {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor} * emits for container children. VITEC list-plugin children are * resolved to their headless JSON. * * @param array $record * @return array */ public static function normaliseRecord(array $record): array { $data = []; foreach ($record as $field => $value) { if (in_array($field, self::ENVELOPE, true)) { continue; } if (in_array($field, self::SYSTEM_FIELDS, true)) { continue; } if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) { continue; } $data[$field] = self::castValue($field, $value); } self::resolvePluginData($record, $data); return [ 'id' => (int)$record['uid'], 'type' => (string)$record['CType'], 'colPos' => (int)($record['colPos'] ?? 0), 'sorting' => (int)($record['sorting'] ?? 0), 'appearance' => [ 'layout' => (string)($record['layout'] ?? ''), 'frameClass' => (string)($record['frame_class'] ?? 'default'), 'spaceBefore' => (string)($record['space_before_class'] ?? ''), 'spaceAfter' => (string)($record['space_after_class'] ?? ''), ], 'data' => (object)$data, ]; } /** * Extract the tt_content uid from a typolink string. * * Handles four forms: * 1. plain numeric → tt_content uid * 2. t3://record?identifier=tt_content&uid=N → uid N * 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored) * 4. t3://page?uid=PAGE#N → uid N from fragment * (link-popup picks a content element on a page; the fragment is * the tt_content uid, the query is the page uid) */ private static function extractTtContentUid(string $link): int { // Form 1: bare numeric uid if (ctype_digit($link)) { return (int)$link; } $parts = parse_url($link); if (!is_array($parts)) { return 0; } // Forms 2 & 3: t3://record?identifier=tt_content&uid=N if (str_starts_with($link, 't3://record') && !empty($parts['query'])) { $params = []; parse_str((string)$parts['query'], $params); $identifier = (string)($params['identifier'] ?? ''); $uid = (int)($params['uid'] ?? 0); if ($identifier === 'tt_content' && $uid > 0) { return $uid; } } // Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment if (str_starts_with($link, 't3://page')) { $fragment = (string)($parts['fragment'] ?? ''); if (ctype_digit($fragment)) { return (int)$fragment; } } return 0; } /** * If the record is a VITEC list-plugin, run its renderer for THIS row * and inject the decoded result under its key. Drops raw pi_flexform. * * @param array $record * @param array $data */ private static function resolvePluginData(array $record, array &$data): void { // v14: plugins are their own CType; legacy elements still carry list_type. $cType = (string)($record['CType'] ?? ''); $listType = (string)($record['list_type'] ?? ''); $key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType : (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null); if ($key === null) { return; } try { [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key]; $renderer = GeneralUtility::makeInstance($rendererClass); if (!method_exists($renderer, 'renderForRecord')) { return; } $json = $renderer->renderForRecord($record); if ($json === '' || $json === null) { return; } $decoded = json_decode($json, true); if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { return; } $data[$jsonKey] = $decoded; unset($data['pi_flexform']); } catch (\Throwable $e) { // leave raw data on failure } } private static function isEmpty(mixed $value): bool { return $value === null || $value === '' || $value === 0 || $value === '0'; } private static function castValue(string $field, mixed $value): mixed { if (in_array($field, ['header_layout'], true)) { return (int)$value; } return $value; } }