[ProductListJsonRenderer::class, 'products'], 'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'], 'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'], 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], ]; /** * Resolve a typolink string to a normalised tt_content JSON element. * * Accepted forms: * - "t3://record?identifier=tt_content&uid=N" * - "" (legacy: bare tt_content uid) * - everything else → null (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. */ private static function extractTtContentUid(string $link): int { if (ctype_digit($link)) { return (int)$link; } if (!str_starts_with($link, 't3://record')) { return 0; } $parts = parse_url($link); if (empty($parts['query'])) { return 0; } $params = []; parse_str((string)$parts['query'], $params); $identifier = (string)($params['identifier'] ?? ''); $uid = (int)($params['uid'] ?? 0); if ($identifier !== 'tt_content' || $uid <= 0) { return 0; } return $uid; } /** * 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 { $listType = (string)($record['list_type'] ?? ''); if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) { return; } try { [$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType]; $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; } }