Files
VITEC-website/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
khaccount eae40f4286 Success-story migration + new plugins + JSON/UX polish
- Success Stories: full migration from old site (48/48, audited),
  markets n:n, quotation content block, columns content block,
  pretty SEO detail URLs via SuccessStoryPathRewrite middleware,
  list/detail split (list page 4 / story page), detailUrl/backUrl
- New plugins: VITEC Locations (grid/list/map + RTE map text),
  VITEC Customer Logos (color/bw logic, only-show-selected),
  VITEC Card (one plugin for product/story/market/solution
  with reloading FlexForm + custom backend preview renderer)
- Eventlist: layout dropdown (list/grid/teaserbar) in settings
- Hero section CB: Images/Video tabs, background video + overlay
- Product JSON: full category rootline (parents), fixed missing
  ConnectionPool import (all-products crash), category tree map
- Backend preview CSS: container-query responsive (narrow columns)
- Docs: ISO architecture spec, root + extension READMEs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:53:33 +02:00

198 lines
6.9 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the VITEC locations as JSON (headless).
*
* Output under content.locations:
* { "variant": "grid|list|map", "mapText": "<p>…</p>"|null, "locations": [ … ] }
*
* Each location follows the frontend contract (id, slug, name, countryCode,
* coordinates, address, contact, links, marker, sorting, active).
*
* `render()` = top-level plugin / page discovery. `renderForRecord()` = one
* specific tt_content row (reused by ContainerChildrenProcessor for nested
* plugins). Exception-safe.
*/
class LocationsJsonRenderer
{
private const TABLE = 'tx_vitec_domain_model_location';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_locationlist') {
return $this->renderForRecord($row);
}
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$ces = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_locationlist', ParameterType::STRING)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
if (empty($ces)) {
return '';
}
return $this->renderForRecord($ces[0]);
}
/**
* @param array<string,mixed> $contentElement
*/
public function renderForRecord(array $contentElement): string
{
try {
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
$settings = $flexFormData['settings'] ?? [];
$variant = (string)($settings['variant'] ?? 'grid');
$mapText = trim((string)($settings['maptext'] ?? ''));
$debugMode = (bool)($settings['debug'] ?? false);
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE);
$rows = $qb
->select('*')
->from(self::TABLE)
->where(
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->orderBy('sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
$locations = array_map(fn(array $r): array => $this->serializeLocation($r), $rows);
$response = [
'variant' => $variant,
'mapText' => $variant === 'map' && $mapText !== '' ? $mapText : null,
'locations' => $locations,
];
if ($debugMode) {
$response['debug'] = ['count' => count($locations), 'settings' => $settings];
}
return (string)json_encode($response);
} catch (\Throwable $e) {
return '';
}
}
/**
* @param array<string,mixed> $r
* @return array<string,mixed>
*/
private function serializeLocation(array $r): array
{
$name = (string)($r['title'] ?? '');
return [
'id' => (int)$r['uid'],
'slug' => (string)($r['slug'] ?? ''),
'name' => $name,
'countryCode' => (string)($r['country_code'] ?? ''),
'coordinates' => [
'latitude' => (float)($r['latitude'] ?? 0),
'longitude' => (float)($r['longitude'] ?? 0),
],
'address' => [
'company' => $this->nullIfEmpty($r['address_company'] ?? ''),
'street' => $this->nullIfEmpty($r['street'] ?? ''),
'additional' => $this->nullIfEmpty($r['address_additional'] ?? ''),
'postalCode' => $this->nullIfEmpty($r['postal_code'] ?? ''),
'city' => $this->nullIfEmpty($r['city'] ?? ''),
'region' => $this->nullIfEmpty($r['region'] ?? ''),
'country' => $this->nullIfEmpty($r['country'] ?? ''),
],
'contact' => [
'phone' => $this->nullIfEmpty($r['phone'] ?? ''),
'fax' => $this->nullIfEmpty($r['fax'] ?? ''),
'email' => $this->nullIfEmpty($r['email'] ?? ''),
],
'links' => [
'contact' => $this->resolveLink((string)($r['contact_link'] ?? '')),
'legal' => $this->resolveLinkLines((string)($r['legal_links'] ?? '')),
],
'marker' => [
'label' => (string)($r['marker_label'] ?? '') !== '' ? (string)$r['marker_label'] : $name,
'color' => (string)($r['marker_color'] ?? '#ff6633'),
'size' => (float)($r['marker_size'] ?? 0.5),
],
'sorting' => (int)($r['sorting'] ?? 0),
'active' => true,
];
}
private function nullIfEmpty(mixed $value): ?string
{
$value = trim((string)$value);
return $value === '' ? null : $value;
}
/** Resolve a typolink parameter (t3://…, page uid, URL) to a URL/path. */
private function resolveLink(string $parameter): ?string
{
$parameter = trim($parameter);
if ($parameter === '') {
return null;
}
try {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$url = $cObj->typoLink_URL(['parameter' => $parameter]);
return $url !== '' ? $url : $parameter;
} catch (\Throwable $e) {
return $parameter;
}
}
/** @return array<int,string> */
private function resolveLinkLines(string $lines): array
{
$out = [];
foreach (preg_split('/\r\n|\r|\n/', $lines) ?: [] as $line) {
$resolved = $this->resolveLink($line);
if ($resolved !== null) {
$out[] = $resolved;
}
}
return $out;
}
}