Zwischenstand vom 29.05.2026
This commit is contained in:
698
composer.lock
generated
698
composer.lock
generated
File diff suppressed because it is too large
Load Diff
152
packages/vitec/Classes/Controller/OgImageController.php
Normal file
152
packages/vitec/Classes/Controller/OgImageController.php
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Controller;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\OgImageGeneratorService;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||||
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||||
|
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend module controller for OG Image generation.
|
||||||
|
*/
|
||||||
|
#[AsController]
|
||||||
|
final class OgImageController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||||
|
private readonly OgImageGeneratorService $ogImageGeneratorService,
|
||||||
|
private readonly FlashMessageService $flashMessageService,
|
||||||
|
private readonly PageRenderer $pageRenderer,
|
||||||
|
private readonly UriBuilder $uriBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the OG image builder form.
|
||||||
|
*/
|
||||||
|
public function indexAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->pageRenderer->addJsFile(
|
||||||
|
'EXT:vitec/Resources/Public/Javascript/og-image-preview.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
$moduleTemplate = $this->moduleTemplateFactory->create($request);
|
||||||
|
|
||||||
|
$moduleTemplate->assignMultiple([
|
||||||
|
'formData' => $this->getDefaultFormData(),
|
||||||
|
'savedImages' => $this->ogImageGeneratorService->getSavedImages(),
|
||||||
|
'presets' => $this->getPresets(),
|
||||||
|
'backgroundImages' => $this->ogImageGeneratorService->getBackgroundImages(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $moduleTemplate->renderResponse('OgImage/Index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles form submission and generates the OG image.
|
||||||
|
*/
|
||||||
|
public function generateAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$parsedBody = $request->getParsedBody();
|
||||||
|
$formData = $parsedBody['ogimage'] ?? [];
|
||||||
|
|
||||||
|
[$success, $message, $filePath] = $this->ogImageGeneratorService->generate($formData);
|
||||||
|
|
||||||
|
$severity = $success
|
||||||
|
? ContextualFeedbackSeverity::OK
|
||||||
|
: ContextualFeedbackSeverity::ERROR;
|
||||||
|
|
||||||
|
$flashMessage = GeneralUtility::makeInstance(
|
||||||
|
FlashMessage::class,
|
||||||
|
$message,
|
||||||
|
$success ? 'OG Image created' : 'Generation failed',
|
||||||
|
$severity,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$this->flashMessageService
|
||||||
|
->getMessageQueueByIdentifier()
|
||||||
|
->addMessage($flashMessage);
|
||||||
|
|
||||||
|
$moduleUrl = (string)$this->uriBuilder->buildUriFromRoute('web_vitecogimage');
|
||||||
|
return new RedirectResponse($moduleUrl, 303);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a base64 PNG preview without saving to disk (AJAX).
|
||||||
|
*/
|
||||||
|
public function previewAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$parsedBody = $request->getParsedBody();
|
||||||
|
$formData = $parsedBody['ogimage'] ?? [];
|
||||||
|
|
||||||
|
[$success, $message, $dataUri] = $this->ogImageGeneratorService->generatePreview($formData);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $success,
|
||||||
|
'message' => $message,
|
||||||
|
'dataUri' => $dataUri,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function getDefaultFormData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'bg_type' => 'color',
|
||||||
|
'bg_color' => '#1a1a2e',
|
||||||
|
'bg_image' => '',
|
||||||
|
'overlay_opacity' => 40,
|
||||||
|
'title' => 'Your Page Title',
|
||||||
|
'title_color' => '#ffffff',
|
||||||
|
'title_size' => 64,
|
||||||
|
'subtitle' => 'A short description of your page',
|
||||||
|
'subtitle_color' => '#cccccc',
|
||||||
|
'subtitle_size' => 32,
|
||||||
|
'label_text' => 'VITEC',
|
||||||
|
'label_bg_color' => '#e63946',
|
||||||
|
'label_text_color' => '#ffffff',
|
||||||
|
'label_position' => 'top-left',
|
||||||
|
'logo_path' => '',
|
||||||
|
'output_format' => 'jpg',
|
||||||
|
'output_quality' => 90,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPresets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dark-blue' => [
|
||||||
|
'label' => 'Dark Blue',
|
||||||
|
'bg_color' => '#1a1a2e',
|
||||||
|
'title_color' => '#ffffff',
|
||||||
|
'subtitle_color' => '#aaaacc',
|
||||||
|
],
|
||||||
|
'light' => [
|
||||||
|
'label' => 'Clean Light',
|
||||||
|
'bg_color' => '#f8f9fa',
|
||||||
|
'title_color' => '#212529',
|
||||||
|
'subtitle_color' => '#6c757d',
|
||||||
|
],
|
||||||
|
'brand-red' => [
|
||||||
|
'label' => 'VITEC Brand Red',
|
||||||
|
'bg_color' => '#1f1f1f',
|
||||||
|
'title_color' => '#ffffff',
|
||||||
|
'subtitle_color' => '#e63946',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\DataProcessing;
|
namespace Evomedien\Vitec\DataProcessing;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
@@ -15,6 +19,11 @@ use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
|
|||||||
* { id, type, colPos, sorting, appearance, data }
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
* `data` only contains non-empty content-relevant fields.
|
* `data` only contains non-empty content-relevant fields.
|
||||||
*
|
*
|
||||||
|
* VITEC list-plugins nested as container children (productlist, productshow,
|
||||||
|
* usecaselist, usecaseshow) are resolved to the SAME JSON the top-level
|
||||||
|
* headless rendering produces, injected into `data` under their respective
|
||||||
|
* key. The raw pi_flexform XML is then dropped from `data`.
|
||||||
|
*
|
||||||
* Exception-safe.
|
* Exception-safe.
|
||||||
*/
|
*/
|
||||||
final class ContainerChildrenProcessor implements DataProcessorInterface
|
final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||||
@@ -27,7 +36,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
|
|
||||||
/** Technical / system / TCA-default fields — never sent to frontend. */
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
private const SYSTEM_FIELDS = [
|
private const SYSTEM_FIELDS = [
|
||||||
// versioning / language / workspace / housekeeping
|
|
||||||
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
'l10n_source', 'l10n_state', 'l10n_parent',
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
't3_origuid', 'tx_impexp_origuid',
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
@@ -38,8 +46,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
'editlock', 'sorting_foreign', 'rowDescription',
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
'spaceBefore', 'spaceAfter', // legacy
|
'spaceBefore', 'spaceAfter', // legacy
|
||||||
// TCA defaults that TYPO3 always sets on every tt_content row,
|
|
||||||
// regardless of CType — rarely relevant to the frontend:
|
|
||||||
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
'bullets_type', 'cols',
|
'bullets_type', 'cols',
|
||||||
'table_delimiter', 'table_enclosure', 'table_header_position',
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
@@ -53,6 +59,19 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
'header_layout',
|
'header_layout',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nested VITEC list-plugins: list_type => [ rendererClass, jsonKey ].
|
||||||
|
* The renderer's renderForRecord() is invoked with the child's own
|
||||||
|
* tt_content row so the result is record-accurate (works with multiple
|
||||||
|
* containers / plugins on the same page).
|
||||||
|
*/
|
||||||
|
private const PLUGIN_RENDERERS = [
|
||||||
|
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
|
||||||
|
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
|
||||||
|
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
|
||||||
|
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
||||||
|
];
|
||||||
|
|
||||||
public function process(
|
public function process(
|
||||||
ContentObjectRenderer $cObj,
|
ContentObjectRenderer $cObj,
|
||||||
array $contentObjectConfiguration,
|
array $contentObjectConfiguration,
|
||||||
@@ -124,6 +143,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
$data[$field] = $this->castValue($field, $value);
|
$data[$field] = $this->castValue($field, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve nested VITEC list-plugins to their headless JSON.
|
||||||
|
$this->resolvePluginData($record, $data);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int)$record['uid'],
|
'id' => (int)$record['uid'],
|
||||||
'type' => (string)$record['CType'],
|
'type' => (string)$record['CType'],
|
||||||
@@ -139,6 +161,48 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the child is a VITEC list-plugin, run its JSON renderer for THIS
|
||||||
|
* record and inject the decoded result under its key. Drops the raw
|
||||||
|
* pi_flexform XML afterwards. Never throws.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $record
|
||||||
|
* @param array<string,mixed> $data
|
||||||
|
*/
|
||||||
|
private 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;
|
||||||
|
|
||||||
|
// The raw FlexForm XML is noise once the plugin is resolved.
|
||||||
|
unset($data['pi_flexform']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Leave the raw data untouched on any failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function isEmpty(mixed $value): bool
|
private function isEmpty(mixed $value): bool
|
||||||
{
|
{
|
||||||
return $value === null
|
return $value === null
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\DataProcessing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||||
|
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect container children grouped by colPos and emit a lean, transport-
|
||||||
|
* ready structure. Each child is normalised to:
|
||||||
|
* { id, type, colPos, sorting, appearance, data }
|
||||||
|
* `data` only contains non-empty content-relevant fields.
|
||||||
|
*
|
||||||
|
* Exception-safe.
|
||||||
|
*/
|
||||||
|
final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||||
|
{
|
||||||
|
/** Fields that go to the envelope (not to `data`). */
|
||||||
|
private const ENVELOPE = [
|
||||||
|
'uid', 'CType', 'colPos', 'sorting',
|
||||||
|
'layout', 'frame_class', 'space_before_class', 'space_after_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Technical / system / TCA-default fields — never sent to frontend. */
|
||||||
|
private const SYSTEM_FIELDS = [
|
||||||
|
// versioning / language / workspace / housekeeping
|
||||||
|
'pid', 'sys_language_uid', 'l18n_parent', 'l18n_diffsource',
|
||||||
|
'l10n_source', 'l10n_state', 'l10n_parent',
|
||||||
|
't3_origuid', 'tx_impexp_origuid',
|
||||||
|
'tx_container_parent',
|
||||||
|
'tstamp', 'crdate', 'cruser_id',
|
||||||
|
'hidden', 'deleted', 'starttime', 'endtime', 'fe_group',
|
||||||
|
't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage',
|
||||||
|
't3ver_id', 't3ver_label', 't3ver_count', 't3ver_tstamp',
|
||||||
|
'editlock', 'sorting_foreign', 'rowDescription',
|
||||||
|
'spaceBefore', 'spaceAfter', // legacy
|
||||||
|
// TCA defaults that TYPO3 always sets on every tt_content row,
|
||||||
|
// regardless of CType — rarely relevant to the frontend:
|
||||||
|
'imagecols', 'sectionIndex', 'linkToTop', 'recursive', 'date',
|
||||||
|
'bullets_type', 'cols',
|
||||||
|
'table_delimiter', 'table_enclosure', 'table_header_position',
|
||||||
|
'table_tfoot', 'table_caption',
|
||||||
|
'filelink_size', 'filelink_sorting', 'filelink_sorting_direction',
|
||||||
|
'uploads_description', 'uploads_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Fields whose 0/empty value is still meaningful. */
|
||||||
|
private const KEEP_IF_ZERO = [
|
||||||
|
'header_layout',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function process(
|
||||||
|
ContentObjectRenderer $cObj,
|
||||||
|
array $contentObjectConfiguration,
|
||||||
|
array $processorConfiguration,
|
||||||
|
array $processedData
|
||||||
|
): array {
|
||||||
|
$as = (string)($processorConfiguration['as'] ?? 'items');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$parentUid = (int)($cObj->data['uid'] ?? 0);
|
||||||
|
if ($parentUid <= 0) {
|
||||||
|
$processedData[$as] = [];
|
||||||
|
return $processedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pid = (int)($cObj->data['pid'] ?? 0);
|
||||||
|
$sysLanguageUid = (int)($cObj->data['sys_language_uid'] ?? 0);
|
||||||
|
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
$rows = $qb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->eq('tx_container_parent', $qb->createNamedParameter($parentUid, Connection::PARAM_INT)),
|
||||||
|
$qb->expr()->eq('pid', $qb->createNamedParameter($pid, Connection::PARAM_INT)),
|
||||||
|
$qb->expr()->eq('sys_language_uid', $qb->createNamedParameter($sysLanguageUid, Connection::PARAM_INT))
|
||||||
|
)
|
||||||
|
->orderBy('colPos')
|
||||||
|
->addOrderBy('sorting')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$byColPos = [];
|
||||||
|
foreach ($rows as $record) {
|
||||||
|
$byColPos[(int)$record['colPos']][] = $this->normalise($record);
|
||||||
|
}
|
||||||
|
ksort($byColPos);
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ($byColPos as $colPos => $contentElements) {
|
||||||
|
$items[] = [
|
||||||
|
'config' => ['colPos' => $colPos],
|
||||||
|
'contentElements' => $contentElements,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedData[$as] = $items;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$processedData[$as] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalise(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 ($this->isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$data[$field] = $this->castValue($field, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$record['uid'],
|
||||||
|
'type' => (string)$record['CType'],
|
||||||
|
'colPos' => (int)$record['colPos'],
|
||||||
|
'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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isEmpty(mixed $value): bool
|
||||||
|
{
|
||||||
|
return $value === null
|
||||||
|
|| $value === ''
|
||||||
|
|| $value === 0
|
||||||
|
|| $value === '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function castValue(string $field, mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (in_array($field, ['header_layout'], true)) {
|
||||||
|
return (int)$value;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
419
packages/vitec/Classes/Service/OgImageGeneratorService.php
Normal file
419
packages/vitec/Classes/Service/OgImageGeneratorService.php
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates 1200×630 OG images using the PHP GD library.
|
||||||
|
*/
|
||||||
|
final class OgImageGeneratorService
|
||||||
|
{
|
||||||
|
private const OG_WIDTH = 1200;
|
||||||
|
private const OG_HEIGHT = 630;
|
||||||
|
|
||||||
|
/** Where generated images are stored relative to the web root */
|
||||||
|
private const SAVE_DIR = 'fileadmin/og-images/';
|
||||||
|
|
||||||
|
/** Font paths (ordered by preference) */
|
||||||
|
private const FONT_BOLD = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf';
|
||||||
|
private const FONT_REG = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf';
|
||||||
|
|
||||||
|
private const PADDING = 80;
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Public API
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and save an OG image.
|
||||||
|
*
|
||||||
|
* @return array{0: bool, 1: string, 2: string} [success, message, publicPath]
|
||||||
|
*/
|
||||||
|
public function generate(array $opts): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$gd = $this->buildImage($opts);
|
||||||
|
|
||||||
|
$saveDir = Environment::getPublicPath() . '/' . self::SAVE_DIR;
|
||||||
|
GeneralUtility::mkdir_deep($saveDir);
|
||||||
|
|
||||||
|
$hashSeed = json_encode($opts, JSON_THROW_ON_ERROR) . microtime(true) . random_int(1000, 9999);
|
||||||
|
$fileName = 'og-' . substr(sha1($hashSeed), 0, 12) . '.' . ($opts['output_format'] ?? 'jpg');
|
||||||
|
$fullPath = $saveDir . $fileName;
|
||||||
|
|
||||||
|
$this->saveImage($gd, $fullPath, $opts['output_format'] ?? 'jpg', (int)($opts['output_quality'] ?? 90));
|
||||||
|
imagedestroy($gd);
|
||||||
|
|
||||||
|
$publicUrl = '/' . self::SAVE_DIR . $fileName;
|
||||||
|
|
||||||
|
return [true, 'Image saved: ' . $publicUrl, $publicUrl];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [false, 'Error: ' . $e->getMessage(), ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a base64 data-URI preview (no file write).
|
||||||
|
*
|
||||||
|
* @return array{0: bool, 1: string, 2: string} [success, message, dataUri]
|
||||||
|
*/
|
||||||
|
public function generatePreview(array $opts): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$gd = $this->buildImage($opts);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
imagepng($gd);
|
||||||
|
$raw = ob_get_clean();
|
||||||
|
imagedestroy($gd);
|
||||||
|
|
||||||
|
return [true, 'OK', 'data:image/png;base64,' . base64_encode($raw)];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [false, $e->getMessage(), ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of previously generated images in the save dir.
|
||||||
|
*/
|
||||||
|
public function getSavedImages(): array
|
||||||
|
{
|
||||||
|
$dir = Environment::getPublicPath() . '/' . self::SAVE_DIR;
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = glob($dir . '*.{jpg,jpeg,png}', GLOB_BRACE) ?: [];
|
||||||
|
usort($files, static fn($a, $b) => filemtime($b) <=> filemtime($a));
|
||||||
|
|
||||||
|
return array_map(static fn($f) => [
|
||||||
|
'path' => '/' . self::SAVE_DIR . basename($f),
|
||||||
|
'name' => basename($f),
|
||||||
|
'created' => date('Y-m-d H:i', filemtime($f)),
|
||||||
|
], array_slice($files, 0, 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return selectable background images from fileadmin/og-backgrounds/.
|
||||||
|
*/
|
||||||
|
public function getBackgroundImages(): array
|
||||||
|
{
|
||||||
|
$dir = Environment::getPublicPath() . '/fileadmin/og-backgrounds/';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = glob($dir . '*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [];
|
||||||
|
|
||||||
|
return array_map(static fn($f) => [
|
||||||
|
'path' => '/fileadmin/og-backgrounds/' . basename($f),
|
||||||
|
'label' => basename($f),
|
||||||
|
], $files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Core image builder
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** @return \GdImage */
|
||||||
|
private function buildImage(array $opts): \GdImage
|
||||||
|
{
|
||||||
|
$canvas = imagecreatetruecolor(self::OG_WIDTH, self::OG_HEIGHT);
|
||||||
|
if ($canvas === false) {
|
||||||
|
throw new \RuntimeException('imagecreatetruecolor failed – GD not available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable alpha blending
|
||||||
|
imagealphablending($canvas, true);
|
||||||
|
imagesavealpha($canvas, true);
|
||||||
|
|
||||||
|
// 1. Background
|
||||||
|
$this->drawBackground($canvas, $opts);
|
||||||
|
|
||||||
|
// 2. Optional dark overlay when bg_image is set
|
||||||
|
if (!empty($opts['bg_image']) && ($opts['bg_type'] ?? '') === 'image') {
|
||||||
|
$opacity = max(0, min(100, (int)($opts['overlay_opacity'] ?? 40)));
|
||||||
|
$this->drawOverlay($canvas, $opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Label / badge
|
||||||
|
if (!empty(trim($opts['label_text'] ?? ''))) {
|
||||||
|
$this->drawLabel($canvas, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Title
|
||||||
|
if (!empty(trim($opts['title'] ?? ''))) {
|
||||||
|
$this->drawTitle($canvas, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Subtitle
|
||||||
|
if (!empty(trim($opts['subtitle'] ?? ''))) {
|
||||||
|
$this->drawSubtitle($canvas, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Logo (optional)
|
||||||
|
if (!empty($opts['logo_path'])) {
|
||||||
|
$this->drawLogo($canvas, $opts['logo_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Drawing helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function drawBackground(\GdImage $canvas, array $opts): void
|
||||||
|
{
|
||||||
|
$bgType = $opts['bg_type'] ?? 'color';
|
||||||
|
|
||||||
|
if ($bgType === 'image' && !empty($opts['bg_image'])) {
|
||||||
|
$imagePath = $this->resolvePath($opts['bg_image']);
|
||||||
|
$src = $this->loadImageFromPath($imagePath);
|
||||||
|
|
||||||
|
if ($src !== null) {
|
||||||
|
imagecopyresampled(
|
||||||
|
$canvas, $src,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
self::OG_WIDTH, self::OG_HEIGHT,
|
||||||
|
imagesx($src), imagesy($src)
|
||||||
|
);
|
||||||
|
imagedestroy($src);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solid colour fallback
|
||||||
|
$hex = ltrim($opts['bg_color'] ?? '#1a1a2e', '#');
|
||||||
|
[$r, $g, $b] = $this->hexToRgb($hex);
|
||||||
|
$bg = imagecolorallocate($canvas, $r, $g, $b);
|
||||||
|
imagefilledrectangle($canvas, 0, 0, self::OG_WIDTH - 1, self::OG_HEIGHT - 1, $bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawOverlay(\GdImage $canvas, int $opacityPercent): void
|
||||||
|
{
|
||||||
|
// opacity 0 = fully transparent overlay, 100 = solid black
|
||||||
|
$alpha = (int)round(127 - ($opacityPercent / 100 * 127));
|
||||||
|
$color = imagecolorallocatealpha($canvas, 0, 0, 0, $alpha);
|
||||||
|
imagefilledrectangle($canvas, 0, 0, self::OG_WIDTH - 1, self::OG_HEIGHT - 1, $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawLabel(\GdImage $canvas, array $opts): void
|
||||||
|
{
|
||||||
|
$text = strtoupper(trim($opts['label_text'] ?? 'LABEL'));
|
||||||
|
$fontSize = 22;
|
||||||
|
$font = $this->fontPath(true);
|
||||||
|
|
||||||
|
$padding = 14;
|
||||||
|
$bbox = imagettfbbox($fontSize, 0, $font, $text);
|
||||||
|
$textW = abs($bbox[4] - $bbox[0]);
|
||||||
|
$textH = abs($bbox[5] - $bbox[1]);
|
||||||
|
$boxW = $textW + $padding * 2;
|
||||||
|
$boxH = $textH + $padding;
|
||||||
|
|
||||||
|
$position = $opts['label_position'] ?? 'top-left';
|
||||||
|
[$bx, $by] = $this->labelCoords($position, $boxW, $boxH);
|
||||||
|
|
||||||
|
[$br, $bg, $bb] = $this->hexToRgb(ltrim($opts['label_bg_color'] ?? '#e63946', '#'));
|
||||||
|
$bgColor = imagecolorallocate($canvas, $br, $bg, $bb);
|
||||||
|
imagefilledrectangle($canvas, $bx, $by, $bx + $boxW, $by + $boxH, $bgColor);
|
||||||
|
|
||||||
|
[$tr, $tg, $tb] = $this->hexToRgb(ltrim($opts['label_text_color'] ?? '#ffffff', '#'));
|
||||||
|
$textColor = imagecolorallocate($canvas, $tr, $tg, $tb);
|
||||||
|
imagettftext($canvas, $fontSize, 0, $bx + $padding, $by + $textH + (int)($padding / 2), $textColor, $font, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawTitle(\GdImage $canvas, array $opts): void
|
||||||
|
{
|
||||||
|
$text = trim($opts['title'] ?? '');
|
||||||
|
$fontSize = max(20, min(120, (int)($opts['title_size'] ?? 64)));
|
||||||
|
$font = $this->fontPath(true);
|
||||||
|
$maxWidth = self::OG_WIDTH - self::PADDING * 2;
|
||||||
|
|
||||||
|
[$r, $g, $b] = $this->hexToRgb(ltrim($opts['title_color'] ?? '#ffffff', '#'));
|
||||||
|
$color = imagecolorallocate($canvas, $r, $g, $b);
|
||||||
|
|
||||||
|
$lines = $this->wrapText($text, $fontSize, $font, $maxWidth);
|
||||||
|
$lineH = (int)($fontSize * 1.3);
|
||||||
|
$startY = $this->titleStartY($opts, count($lines), $lineH, $fontSize);
|
||||||
|
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
$y = $startY + $i * $lineH;
|
||||||
|
imagettftext($canvas, $fontSize, 0, self::PADDING, $y, $color, $font, $line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawSubtitle(\GdImage $canvas, array $opts): void
|
||||||
|
{
|
||||||
|
$text = trim($opts['subtitle'] ?? '');
|
||||||
|
$fontSize = max(14, min(80, (int)($opts['subtitle_size'] ?? 32)));
|
||||||
|
$font = $this->fontPath(false);
|
||||||
|
$maxWidth = self::OG_WIDTH - self::PADDING * 2;
|
||||||
|
|
||||||
|
[$r, $g, $b] = $this->hexToRgb(ltrim($opts['subtitle_color'] ?? '#cccccc', '#'));
|
||||||
|
$color = imagecolorallocate($canvas, $r, $g, $b);
|
||||||
|
|
||||||
|
$lines = $this->wrapText($text, $fontSize, $font, $maxWidth);
|
||||||
|
$lineH = (int)($fontSize * 1.4);
|
||||||
|
$startY = $this->subtitleStartY($opts, $fontSize);
|
||||||
|
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
$y = $startY + $i * $lineH;
|
||||||
|
imagettftext($canvas, $fontSize, 0, self::PADDING, $y, $color, $font, $line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawLogo(\GdImage $canvas, string $logoPath): void
|
||||||
|
{
|
||||||
|
$path = $this->resolvePath($logoPath);
|
||||||
|
$src = $this->loadImageFromPath($path);
|
||||||
|
if ($src === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logoW = 200;
|
||||||
|
$logoH = (int)(imagesy($src) * ($logoW / imagesx($src)));
|
||||||
|
$x = self::OG_WIDTH - self::PADDING - $logoW;
|
||||||
|
$y = self::OG_HEIGHT - self::PADDING - $logoH;
|
||||||
|
|
||||||
|
imagecopyresampled($canvas, $src, $x, $y, 0, 0, $logoW, $logoH, imagesx($src), imagesy($src));
|
||||||
|
imagedestroy($src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Layout helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function titleStartY(array $opts, int $lineCount, int $lineH, int $fontSize): int
|
||||||
|
{
|
||||||
|
$totalH = $lineCount * $lineH;
|
||||||
|
$subH = empty(trim($opts['subtitle'] ?? '')) ? 0 : (int)($opts['subtitle_size'] ?? 32) + 20;
|
||||||
|
$block = $totalH + $subH;
|
||||||
|
$center = (int)((self::OG_HEIGHT - $block) / 2);
|
||||||
|
// nudge up slightly so text block feels centered
|
||||||
|
return max(self::PADDING + $fontSize, $center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function subtitleStartY(array $opts, int $fontSize): int
|
||||||
|
{
|
||||||
|
$titleSize = max(20, min(120, (int)($opts['title_size'] ?? 64)));
|
||||||
|
$titleText = trim($opts['title'] ?? '');
|
||||||
|
$font = $this->fontPath(true);
|
||||||
|
$maxWidth = self::OG_WIDTH - self::PADDING * 2;
|
||||||
|
$titleLines = $this->wrapText($titleText, $titleSize, $font, $maxWidth);
|
||||||
|
$titleLineH = (int)($titleSize * 1.3);
|
||||||
|
$titleBlock = count($titleLines) * $titleLineH;
|
||||||
|
|
||||||
|
$subH = empty(trim($opts['subtitle'] ?? '')) ? 0 : $fontSize + 20;
|
||||||
|
$block = $titleBlock + $subH;
|
||||||
|
$center = (int)((self::OG_HEIGHT - $block) / 2);
|
||||||
|
$topOfTitle = max(self::PADDING + $titleSize, $center);
|
||||||
|
|
||||||
|
return $topOfTitle + $titleBlock + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function labelCoords(string $position, int $w, int $h): array
|
||||||
|
{
|
||||||
|
$pad = self::PADDING;
|
||||||
|
return match ($position) {
|
||||||
|
'top-right' => [self::OG_WIDTH - $pad - $w, $pad],
|
||||||
|
'bottom-left' => [$pad, self::OG_HEIGHT - $pad - $h],
|
||||||
|
'bottom-right' => [self::OG_WIDTH - $pad - $w, self::OG_HEIGHT - $pad - $h],
|
||||||
|
default => [$pad, $pad], // top-left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Utility helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Wrap text to fit within $maxWidth px */
|
||||||
|
private function wrapText(string $text, int $fontSize, string $font, int $maxWidth): array
|
||||||
|
{
|
||||||
|
$words = explode(' ', $text);
|
||||||
|
$lines = [];
|
||||||
|
$current = '';
|
||||||
|
|
||||||
|
foreach ($words as $word) {
|
||||||
|
$test = $current !== '' ? "$current $word" : $word;
|
||||||
|
$bbox = imagettfbbox($fontSize, 0, $font, $test);
|
||||||
|
$w = abs($bbox[4] - $bbox[0]);
|
||||||
|
|
||||||
|
if ($w > $maxWidth && $current !== '') {
|
||||||
|
$lines[] = $current;
|
||||||
|
$current = $word;
|
||||||
|
} else {
|
||||||
|
$current = $test;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current !== '') {
|
||||||
|
$lines[] = $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines ?: [''];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fontPath(bool $bold): string
|
||||||
|
{
|
||||||
|
$path = $bold ? self::FONT_BOLD : self::FONT_REG;
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$path = self::FONT_REG; // fallback to regular
|
||||||
|
}
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
throw new \RuntimeException("TTF font not found at $path");
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hexToRgb(string $hex): array
|
||||||
|
{
|
||||||
|
$hex = ltrim($hex, '#');
|
||||||
|
if (strlen($hex) === 3) {
|
||||||
|
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
hexdec(substr($hex, 0, 2)),
|
||||||
|
hexdec(substr($hex, 2, 2)),
|
||||||
|
hexdec(substr($hex, 4, 2)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePath(string $path): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($path, '/')) {
|
||||||
|
return Environment::getPublicPath() . $path;
|
||||||
|
}
|
||||||
|
return Environment::getPublicPath() . '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return \GdImage|null */
|
||||||
|
private function loadImageFromPath(string $path): ?\GdImage
|
||||||
|
{
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
return match ($ext) {
|
||||||
|
'jpg', 'jpeg' => imagecreatefromjpeg($path) ?: null,
|
||||||
|
'png' => imagecreatefrompng($path) ?: null,
|
||||||
|
'gif' => imagecreatefromgif($path) ?: null,
|
||||||
|
'webp' => imagecreatefromwebp($path) ?: null,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveImage(\GdImage $gd, string $path, string $format, int $quality): void
|
||||||
|
{
|
||||||
|
match (strtolower($format)) {
|
||||||
|
'png' => imagepng($gd, $path, max(0, min(9, (int)(9 - $quality / 11)))),
|
||||||
|
default => imagejpeg($gd, $path, $quality),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,23 +16,19 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserFunc to render product list as JSON for headless
|
* UserFunc to render product list as JSON for headless.
|
||||||
|
*
|
||||||
|
* `render()` performs page discovery (used for top-level list plugins via
|
||||||
|
* TypoScript). `renderForRecord()` processes one specific tt_content row and
|
||||||
|
* is used both internally and by ContainerChildrenProcessor to resolve a
|
||||||
|
* product-list plugin nested inside a b13 container.
|
||||||
*/
|
*/
|
||||||
class ProductListJsonRenderer
|
class ProductListJsonRenderer
|
||||||
{
|
{
|
||||||
public function render(string $content, array $conf): string
|
public function render(string $content, array $conf): string
|
||||||
{
|
{
|
||||||
// IMPORTANT: Headless creates new cObj contexts when rendering JSON fields,
|
|
||||||
// losing the current content element context. Also, Extbase repositories don't work
|
|
||||||
// in UserFunc context because the full Extbase framework isn't bootstrapped.
|
|
||||||
//
|
|
||||||
// Solution: Query tt_content directly to find the plugin configuration,
|
|
||||||
// then use direct database queries for products instead of Extbase repositories.
|
|
||||||
|
|
||||||
// Get current page UID
|
|
||||||
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
// Query tt_content for vitec_productlist on this page
|
|
||||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('tt_content');
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
@@ -49,95 +45,93 @@ class ProductListJsonRenderer
|
|||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
if (empty($contentElements)) {
|
if (empty($contentElements)) {
|
||||||
return json_encode(['debug' => 'No vitec_productlist on page ' . $pageId]);
|
// Not a product-list page: emit nothing so headless removes the key.
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the first one (there should typically be only one)
|
return $this->renderForRecord($contentElements[0]);
|
||||||
$contentElement = $contentElements[0];
|
}
|
||||||
|
|
||||||
// Parse FlexForm
|
/**
|
||||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
* Render exactly the given tt_content row (the product-list plugin element).
|
||||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
* Exception-safe: returns '' on any failure.
|
||||||
$settings = $flexFormData['settings'] ?? [];
|
*
|
||||||
|
* @param array<string,mixed> $contentElement tt_content row of the plugin
|
||||||
|
*/
|
||||||
|
public function renderForRecord(array $contentElement): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Parse FlexForm of THIS element
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
// Extract category UIDs and debug setting
|
$categoryUids = array_filter(
|
||||||
$categoryUids = array_filter(
|
array_map('intval', explode(',', (string)($settings['categories'] ?? '')))
|
||||||
array_map('intval', explode(',', (string)($settings['categories'] ?? '')))
|
|
||||||
);
|
|
||||||
$debugMode = (bool)($settings['debug'] ?? false);
|
|
||||||
$allProducts = (bool)($settings['allproducts'] ?? false);
|
|
||||||
|
|
||||||
// Extbase repositories don't work in UserFunc context
|
|
||||||
// Use direct database query instead
|
|
||||||
$productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
|
||||||
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
|
||||||
|
|
||||||
$productQuery = $productQueryBuilder
|
|
||||||
->select('p.*')
|
|
||||||
->from('tx_vitec_domain_model_product', 'p')
|
|
||||||
->where(
|
|
||||||
$productQueryBuilder->expr()->eq('p.deleted', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.hidden', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.legacy', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.supportproduct', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.hideonwebsite', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.hideonproducts', 0),
|
|
||||||
$productQueryBuilder->expr()->eq('p.subproduct', 0)
|
|
||||||
);
|
);
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
$allProducts = (bool)($settings['allproducts'] ?? false);
|
||||||
|
|
||||||
// Add category filter if specified and allProducts is not enabled
|
// Extbase repositories don't work in UserFunc context — direct query.
|
||||||
if (!empty($categoryUids) && !$allProducts) {
|
$productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
// Join with sys_category_record_mm to filter by categories
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
$productQuery
|
|
||||||
->join(
|
$productQuery = $productQueryBuilder
|
||||||
'p',
|
->select('p.*')
|
||||||
'sys_category_record_mm',
|
->from('tx_vitec_domain_model_product', 'p')
|
||||||
'mm',
|
->where(
|
||||||
'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
$productQueryBuilder->expr()->eq('p.deleted', 0),
|
||||||
)
|
$productQueryBuilder->expr()->eq('p.hidden', 0),
|
||||||
->andWhere(
|
$productQueryBuilder->expr()->eq('p.legacy', 0),
|
||||||
$productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY))
|
$productQueryBuilder->expr()->eq('p.supportproduct', 0),
|
||||||
)
|
$productQueryBuilder->expr()->eq('p.hideonwebsite', 0),
|
||||||
->groupBy('p.uid');
|
$productQueryBuilder->expr()->eq('p.hideonproducts', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.subproduct', 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($categoryUids) && !$allProducts) {
|
||||||
|
$productQuery
|
||||||
|
->join(
|
||||||
|
'p',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->andWhere(
|
||||||
|
$productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY))
|
||||||
|
)
|
||||||
|
->groupBy('p.uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = $productQuery->executeQuery()->fetchAllAssociative();
|
||||||
|
|
||||||
|
$productsData = [];
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$productsData[] = $this->serializeProduct($product);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
return json_encode([
|
||||||
|
'products' => $productsData,
|
||||||
|
'debug' => [
|
||||||
|
'pageId' => (int)($GLOBALS['TSFE']->id ?? 0),
|
||||||
|
'categoryUids' => $categoryUids,
|
||||||
|
'productCount' => count($productsData),
|
||||||
|
'settings' => $settings,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($productsData);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$products = $productQuery->executeQuery()->fetchAllAssociative();
|
|
||||||
|
|
||||||
// Serialize products (they're already associative arrays from the query)
|
|
||||||
$productsData = [];
|
|
||||||
foreach ($products as $product) {
|
|
||||||
$productsData[] = $this->serializeProduct($product);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If debug mode is enabled, return object with products and debug info
|
|
||||||
if ($debugMode) {
|
|
||||||
return json_encode([
|
|
||||||
'products' => $productsData,
|
|
||||||
'debug' => [
|
|
||||||
'pageId' => $pageId,
|
|
||||||
'categoryUids' => $categoryUids,
|
|
||||||
'productCount' => count($productsData),
|
|
||||||
'settings' => $settings
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise return products array directly (backward compatible)
|
|
||||||
return json_encode($productsData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a single product DB row to the full headless JSON structure.
|
* Serialize a single product DB row to the full headless JSON structure.
|
||||||
*
|
*
|
||||||
* Includes every field declared on the Product domain model
|
* @param array<string,mixed> $product
|
||||||
* (Evomedien\Vitec\Domain\Model\Product) plus all fully resolved
|
|
||||||
* relations (categories, productimage, downloads, ogimage, relatedprodukt).
|
|
||||||
*
|
|
||||||
* DB-only columns that are NOT part of the domain model
|
|
||||||
* (cta, links, sorting1-5, key1-3, apptext1-3, productlayout, image,
|
|
||||||
* relatedimage, system fields) are intentionally omitted.
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $product Associative DB row of tx_vitec_domain_model_product
|
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
protected function serializeProduct(array $product): array
|
protected function serializeProduct(array $product): array
|
||||||
@@ -145,10 +139,8 @@ class ProductListJsonRenderer
|
|||||||
$uid = (int)$product['uid'];
|
$uid = (int)$product['uid'];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// --- identifier ---
|
|
||||||
'uid' => $uid,
|
'uid' => $uid,
|
||||||
|
|
||||||
// --- scalar string fields (Product domain model) ---
|
|
||||||
'title' => (string)($product['title'] ?? ''),
|
'title' => (string)($product['title'] ?? ''),
|
||||||
'slug' => (string)($product['slug'] ?? ''),
|
'slug' => (string)($product['slug'] ?? ''),
|
||||||
'urltitle' => (string)($product['urltitle'] ?? ''),
|
'urltitle' => (string)($product['urltitle'] ?? ''),
|
||||||
@@ -166,7 +158,6 @@ class ProductListJsonRenderer
|
|||||||
'contentelement' => (string)($product['contentelement'] ?? ''),
|
'contentelement' => (string)($product['contentelement'] ?? ''),
|
||||||
'contentelementcta' => (string)($product['contentelementcta'] ?? ''),
|
'contentelementcta' => (string)($product['contentelementcta'] ?? ''),
|
||||||
|
|
||||||
// --- boolean flags (Product domain model) ---
|
|
||||||
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||||
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||||
'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false),
|
'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false),
|
||||||
@@ -176,10 +167,8 @@ class ProductListJsonRenderer
|
|||||||
'supportproduct' => (bool)($product['supportproduct'] ?? false),
|
'supportproduct' => (bool)($product['supportproduct'] ?? false),
|
||||||
'subproduct' => (bool)($product['subproduct'] ?? false),
|
'subproduct' => (bool)($product['subproduct'] ?? false),
|
||||||
|
|
||||||
// --- convenience link (kept for backward compatibility) ---
|
|
||||||
'link' => '/product/' . (string)($product['slug'] ?? ''),
|
'link' => '/product/' . (string)($product['slug'] ?? ''),
|
||||||
|
|
||||||
// --- fully resolved relations ---
|
|
||||||
'categories' => $this->getProductCategories($uid),
|
'categories' => $this->getProductCategories($uid),
|
||||||
'images' => $this->getProductImages($uid),
|
'images' => $this->getProductImages($uid),
|
||||||
'downloads' => $this->getProductDownloads($uid),
|
'downloads' => $this->getProductDownloads($uid),
|
||||||
@@ -188,10 +177,6 @@ class ProductListJsonRenderer
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get product images from FAL (sys_file_reference)
|
|
||||||
* Processes images through ImageService and generates srcset for responsive images
|
|
||||||
*/
|
|
||||||
protected function getProductImages(int $productUid): array
|
protected function getProductImages(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
@@ -217,10 +202,8 @@ class ProductListJsonRenderer
|
|||||||
$images = [];
|
$images = [];
|
||||||
foreach ($fileReferences as $fileRefData) {
|
foreach ($fileReferences as $fileRefData) {
|
||||||
try {
|
try {
|
||||||
// Get FAL FileReference object
|
|
||||||
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
// Define image sizes for srcset
|
|
||||||
$sizes = [
|
$sizes = [
|
||||||
'small' => ['width' => 400, 'height' => null],
|
'small' => ['width' => 400, 'height' => null],
|
||||||
'medium' => ['width' => 800, 'height' => null],
|
'medium' => ['width' => 800, 'height' => null],
|
||||||
@@ -247,7 +230,6 @@ class ProductListJsonRenderer
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get original/default image
|
|
||||||
$defaultProcessed = $imageService->applyProcessingInstructions(
|
$defaultProcessed = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
@@ -267,7 +249,6 @@ class ProductListJsonRenderer
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Skip images that can't be processed
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,8 +257,6 @@ class ProductListJsonRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the single Open Graph image (ogimage) for a product, or null.
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>|null
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
protected function getProductOgImage(int $productUid): ?array
|
protected function getProductOgImage(int $productUid): ?array
|
||||||
@@ -331,9 +310,6 @@ class ProductListJsonRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get categories for a product (resolved sys_category records).
|
|
||||||
*/
|
|
||||||
protected function getProductCategories(int $productUid): array
|
protected function getProductCategories(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
@@ -369,9 +345,6 @@ class ProductListJsonRenderer
|
|||||||
}, $categories);
|
}, $categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all downloads for a product (resolved via tx_vitec_product_download_mm).
|
|
||||||
*/
|
|
||||||
protected function getProductDownloads(int $productUid): array
|
protected function getProductDownloads(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
@@ -400,7 +373,6 @@ class ProductListJsonRenderer
|
|||||||
foreach ($downloads as $download) {
|
foreach ($downloads as $download) {
|
||||||
$fileInfo = null;
|
$fileInfo = null;
|
||||||
|
|
||||||
// Get file information from FAL if file reference exists
|
|
||||||
if (!empty($download['file'])) {
|
if (!empty($download['file'])) {
|
||||||
$fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_file_reference');
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
@@ -448,12 +420,6 @@ class ProductListJsonRenderer
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get related products (resolved via tx_vitec_product_related_mm).
|
|
||||||
*
|
|
||||||
* Returns a shallow representation (no nested relations) to avoid
|
|
||||||
* infinite recursion between mutually related products.
|
|
||||||
*/
|
|
||||||
protected function getRelatedProducts(int $productUid): array
|
protected function getRelatedProducts(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
@@ -480,8 +446,6 @@ class ProductListJsonRenderer
|
|||||||
$result = [];
|
$result = [];
|
||||||
foreach ($related as $rel) {
|
foreach ($related as $rel) {
|
||||||
$relUid = (int)$rel['uid'];
|
$relUid = (int)$rel['uid'];
|
||||||
$images = $this->getProductImages($relUid);
|
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'uid' => $relUid,
|
'uid' => $relUid,
|
||||||
'title' => (string)($rel['title'] ?? ''),
|
'title' => (string)($rel['title'] ?? ''),
|
||||||
@@ -490,7 +454,7 @@ class ProductListJsonRenderer
|
|||||||
'teaser' => (string)($rel['teaser'] ?? ''),
|
'teaser' => (string)($rel['teaser'] ?? ''),
|
||||||
'description' => (string)($rel['description'] ?? ''),
|
'description' => (string)($rel['description'] ?? ''),
|
||||||
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
||||||
'images' => $images,
|
'images' => $this->getProductImages($relUid),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
499
packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048
Executable file
499
packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048
Executable file
@@ -0,0 +1,499 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use Evomedien\Vitec\Domain\Repository\ProductRepository;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||||
|
use TYPO3\CMS\Core\Resource\FileReference;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render product list as JSON for headless
|
||||||
|
*/
|
||||||
|
class ProductListJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
// IMPORTANT: Headless creates new cObj contexts when rendering JSON fields,
|
||||||
|
// losing the current content element context. Also, Extbase repositories don't work
|
||||||
|
// in UserFunc context because the full Extbase framework isn't bootstrapped.
|
||||||
|
//
|
||||||
|
// Solution: Query tt_content directly to find the plugin configuration,
|
||||||
|
// then use direct database queries for products instead of Extbase repositories.
|
||||||
|
|
||||||
|
// Get current page UID
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
// Query tt_content for vitec_productlist on this page
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
return json_encode(['debug' => 'No vitec_productlist on page ' . $pageId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the first one (there should typically be only one)
|
||||||
|
$contentElement = $contentElements[0];
|
||||||
|
|
||||||
|
// Parse FlexForm
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
// Extract category UIDs and debug setting
|
||||||
|
$categoryUids = array_filter(
|
||||||
|
array_map('intval', explode(',', (string)($settings['categories'] ?? '')))
|
||||||
|
);
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
$allProducts = (bool)($settings['allproducts'] ?? false);
|
||||||
|
|
||||||
|
// Extbase repositories don't work in UserFunc context
|
||||||
|
// Use direct database query instead
|
||||||
|
$productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$productQuery = $productQueryBuilder
|
||||||
|
->select('p.*')
|
||||||
|
->from('tx_vitec_domain_model_product', 'p')
|
||||||
|
->where(
|
||||||
|
$productQueryBuilder->expr()->eq('p.deleted', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.hidden', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.legacy', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.supportproduct', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.hideonwebsite', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.hideonproducts', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('p.subproduct', 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add category filter if specified and allProducts is not enabled
|
||||||
|
if (!empty($categoryUids) && !$allProducts) {
|
||||||
|
// Join with sys_category_record_mm to filter by categories
|
||||||
|
$productQuery
|
||||||
|
->join(
|
||||||
|
'p',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = p.uid AND mm.tablenames = ' . $productQueryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $productQueryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->andWhere(
|
||||||
|
$productQueryBuilder->expr()->in('mm.uid_local', $productQueryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY))
|
||||||
|
)
|
||||||
|
->groupBy('p.uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = $productQuery->executeQuery()->fetchAllAssociative();
|
||||||
|
|
||||||
|
// Serialize products (they're already associative arrays from the query)
|
||||||
|
$productsData = [];
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$productsData[] = $this->serializeProduct($product);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If debug mode is enabled, return object with products and debug info
|
||||||
|
if ($debugMode) {
|
||||||
|
return json_encode([
|
||||||
|
'products' => $productsData,
|
||||||
|
'debug' => [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'categoryUids' => $categoryUids,
|
||||||
|
'productCount' => count($productsData),
|
||||||
|
'settings' => $settings
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return products array directly (backward compatible)
|
||||||
|
return json_encode($productsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a single product DB row to the full headless JSON structure.
|
||||||
|
*
|
||||||
|
* Includes every field declared on the Product domain model
|
||||||
|
* (Evomedien\Vitec\Domain\Model\Product) plus all fully resolved
|
||||||
|
* relations (categories, productimage, downloads, ogimage, relatedprodukt).
|
||||||
|
*
|
||||||
|
* DB-only columns that are NOT part of the domain model
|
||||||
|
* (cta, links, sorting1-5, key1-3, apptext1-3, productlayout, image,
|
||||||
|
* relatedimage, system fields) are intentionally omitted.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $product Associative DB row of tx_vitec_domain_model_product
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeProduct(array $product): array
|
||||||
|
{
|
||||||
|
$uid = (int)$product['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
// --- identifier ---
|
||||||
|
'uid' => $uid,
|
||||||
|
|
||||||
|
// --- scalar string fields (Product domain model) ---
|
||||||
|
'title' => (string)($product['title'] ?? ''),
|
||||||
|
'slug' => (string)($product['slug'] ?? ''),
|
||||||
|
'urltitle' => (string)($product['urltitle'] ?? ''),
|
||||||
|
'seotitle' => (string)($product['seotitle'] ?? ''),
|
||||||
|
'seometa' => (string)($product['seometa'] ?? ''),
|
||||||
|
'keywords' => (string)($product['keywords'] ?? ''),
|
||||||
|
'structureddata' => (string)($product['structureddata'] ?? ''),
|
||||||
|
'teaser' => (string)($product['teaser'] ?? ''),
|
||||||
|
'subtitle' => (string)($product['subtitle'] ?? ''),
|
||||||
|
'video' => (string)($product['video'] ?? ''),
|
||||||
|
'applications' => (string)($product['applications'] ?? ''),
|
||||||
|
'description' => (string)($product['description'] ?? ''),
|
||||||
|
'highlights' => (string)($product['highlights'] ?? ''),
|
||||||
|
'shortcutpid' => (string)($product['shortcutpid'] ?? ''),
|
||||||
|
'contentelement' => (string)($product['contentelement'] ?? ''),
|
||||||
|
'contentelementcta' => (string)($product['contentelementcta'] ?? ''),
|
||||||
|
|
||||||
|
// --- boolean flags (Product domain model) ---
|
||||||
|
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||||
|
'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false),
|
||||||
|
'hideonproducts' => (bool)($product['hideonproducts'] ?? false),
|
||||||
|
'shortcut' => (bool)($product['shortcut'] ?? false),
|
||||||
|
'legacy' => (bool)($product['legacy'] ?? false),
|
||||||
|
'supportproduct' => (bool)($product['supportproduct'] ?? false),
|
||||||
|
'subproduct' => (bool)($product['subproduct'] ?? false),
|
||||||
|
|
||||||
|
// --- convenience link (kept for backward compatibility) ---
|
||||||
|
'link' => '/product/' . (string)($product['slug'] ?? ''),
|
||||||
|
|
||||||
|
// --- fully resolved relations ---
|
||||||
|
'categories' => $this->getProductCategories($uid),
|
||||||
|
'images' => $this->getProductImages($uid),
|
||||||
|
'downloads' => $this->getProductDownloads($uid),
|
||||||
|
'ogimage' => $this->getProductOgImage($uid),
|
||||||
|
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product images from FAL (sys_file_reference)
|
||||||
|
* Processes images through ImageService and generates srcset for responsive images
|
||||||
|
*/
|
||||||
|
protected function getProductImages(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileReferences = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.uid_local', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
|
||||||
|
$images = [];
|
||||||
|
foreach ($fileReferences as $fileRefData) {
|
||||||
|
try {
|
||||||
|
// Get FAL FileReference object
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
// Define image sizes for srcset
|
||||||
|
$sizes = [
|
||||||
|
'small' => ['width' => 400, 'height' => null],
|
||||||
|
'medium' => ['width' => 800, 'height' => null],
|
||||||
|
'large' => ['width' => 1200, 'height' => null],
|
||||||
|
'xlarge' => ['width' => 1600, 'height' => null],
|
||||||
|
];
|
||||||
|
|
||||||
|
$srcset = [];
|
||||||
|
foreach ($sizes as $sizeName => $dimensions) {
|
||||||
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
[
|
||||||
|
'width' => $dimensions['width'],
|
||||||
|
'height' => $dimensions['height'],
|
||||||
|
'crop' => $fileRefData['crop'] ?? null
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$imageUri = $imageService->getImageUri($processedImage);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageUri,
|
||||||
|
'width' => $dimensions['width'],
|
||||||
|
'descriptor' => $dimensions['width'] . 'w'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original/default image
|
||||||
|
$defaultProcessed = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
$images[] = [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($defaultProcessed),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type')
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Skip images that can't be processed
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $images;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the single Open Graph image (ogimage) for a product, or null.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getProductOgImage(int $productUid): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRefData = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRefData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
$processed = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($processed),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories for a product (resolved sys_category records).
|
||||||
|
*/
|
||||||
|
protected function getProductCategories(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(static function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'] ?? '',
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all downloads for a product (resolved via tx_vitec_product_download_mm).
|
||||||
|
*/
|
||||||
|
protected function getProductDownloads(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_download');
|
||||||
|
|
||||||
|
$downloads = $queryBuilder
|
||||||
|
->select('d.*')
|
||||||
|
->from('tx_vitec_domain_model_download', 'd')
|
||||||
|
->join(
|
||||||
|
'd',
|
||||||
|
'tx_vitec_product_download_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = d.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('d.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hidden', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hideonwebsite', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($downloads as $download) {
|
||||||
|
$fileInfo = null;
|
||||||
|
|
||||||
|
// Get file information from FAL if file reference exists
|
||||||
|
if (!empty($download['file'])) {
|
||||||
|
$fileQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRef = $fileQueryBuilder
|
||||||
|
->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type')
|
||||||
|
->from('sys_file_reference', 'fr')
|
||||||
|
->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid')
|
||||||
|
->where(
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter((int)$download['uid'], ParameterType::INTEGER)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.tablenames', $fileQueryBuilder->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.fieldname', $fileQueryBuilder->createNamedParameter('file', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.deleted', 0),
|
||||||
|
$fileQueryBuilder->expr()->eq('f.missing', 0)
|
||||||
|
)
|
||||||
|
->orderBy('fr.sorting_foreign', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if ($fileRef) {
|
||||||
|
$fileInfo = [
|
||||||
|
'uid' => (int)$fileRef['file_uid'],
|
||||||
|
'name' => $fileRef['name'],
|
||||||
|
'url' => '/fileadmin' . $fileRef['identifier'],
|
||||||
|
'size' => (int)$fileRef['size'],
|
||||||
|
'extension' => $fileRef['extension'],
|
||||||
|
'mimeType' => $fileRef['mime_type'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'uid' => (int)$download['uid'],
|
||||||
|
'title' => $download['title'] ?? '',
|
||||||
|
'slug' => $download['slug'] ?? '',
|
||||||
|
'teaser' => $download['teaser'] ?? '',
|
||||||
|
'description' => $download['description'] ?? '',
|
||||||
|
'keywords' => $download['keywords'] ?? '',
|
||||||
|
'icon' => $download['icon'] ?? '',
|
||||||
|
'file' => $fileInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get related products (resolved via tx_vitec_product_related_mm).
|
||||||
|
*
|
||||||
|
* Returns a shallow representation (no nested relations) to avoid
|
||||||
|
* infinite recursion between mutually related products.
|
||||||
|
*/
|
||||||
|
protected function getRelatedProducts(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$related = $queryBuilder
|
||||||
|
->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description')
|
||||||
|
->from('tx_vitec_domain_model_product', 'p')
|
||||||
|
->join(
|
||||||
|
'p',
|
||||||
|
'tx_vitec_product_related_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = p.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('p.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('p.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($related as $rel) {
|
||||||
|
$relUid = (int)$rel['uid'];
|
||||||
|
$images = $this->getProductImages($relUid);
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'uid' => $relUid,
|
||||||
|
'title' => (string)($rel['title'] ?? ''),
|
||||||
|
'slug' => (string)($rel['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($rel['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($rel['teaser'] ?? ''),
|
||||||
|
'description' => (string)($rel['description'] ?? ''),
|
||||||
|
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
||||||
|
'images' => $images,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,25 +14,22 @@ use TYPO3\CMS\Core\Imaging\ImageService;
|
|||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserFunc to render single product data as JSON for headless output
|
* UserFunc to render a single product as JSON for headless output.
|
||||||
|
*
|
||||||
|
* `render()` performs page discovery (top-level plugin via TypoScript).
|
||||||
|
* `renderForRecord()` processes one specific tt_content row and is reused
|
||||||
|
* by ContainerChildrenProcessor for product-show plugins nested in a
|
||||||
|
* b13 container.
|
||||||
*/
|
*/
|
||||||
class ProductShowJsonRenderer
|
class ProductShowJsonRenderer
|
||||||
{
|
{
|
||||||
public function render(string $content, array $conf): string
|
public function render(string $content, array $conf): string
|
||||||
{
|
{
|
||||||
$pageId = (int)$GLOBALS['TSFE']->id;
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
// DEBUG: Log that the UserFunc is being called
|
|
||||||
$debugInfo = [
|
|
||||||
'userFuncCalled' => true,
|
|
||||||
'pageId' => $pageId,
|
|
||||||
'requestUri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Query tt_content for vitec_productshow on this page
|
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('tt_content');
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
$contentElements = $queryBuilder
|
$contentElements = $queryBuilder
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('tt_content')
|
->from('tt_content')
|
||||||
@@ -44,140 +41,161 @@ class ProductShowJsonRenderer
|
|||||||
)
|
)
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
$debugInfo['contentElementsFound'] = count($contentElements);
|
|
||||||
|
|
||||||
if (empty($contentElements)) {
|
if (empty($contentElements)) {
|
||||||
$debugInfo['error'] = 'No vitec_productshow on page';
|
return '';
|
||||||
return json_encode(['debug' => $debugInfo]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the first one
|
return $this->renderForRecord($contentElements[0]);
|
||||||
$contentElement = $contentElements[0];
|
}
|
||||||
|
|
||||||
// Parse FlexForm
|
/**
|
||||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
* Render exactly the given tt_content row (the product-show plugin element).
|
||||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
* Exception-safe: returns '' on any failure.
|
||||||
$settings = $flexFormData['settings'] ?? [];
|
*
|
||||||
|
* @param array<string,mixed> $contentElement
|
||||||
// Get product UID from FlexForm or route parameter
|
*/
|
||||||
$productUid = (int)($settings['product'] ?? 0);
|
public function renderForRecord(array $contentElement): string
|
||||||
$layout = (int)($settings['layout'] ?? 0);
|
{
|
||||||
$debugMode = (bool)($settings['debug'] ?? false);
|
try {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
// If no product selected in FlexForm, try to get from route parameter
|
|
||||||
if (!$productUid) {
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
// Get the product parameter from GET request
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
|
|
||||||
|
$productUid = (int)($settings['product'] ?? 0);
|
||||||
if ($productParam) {
|
$layout = (int)($settings['layout'] ?? 0);
|
||||||
// If it's a slug, resolve it to UID
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
if (!is_numeric($productParam)) {
|
|
||||||
$slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
if (!$productUid) {
|
||||||
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
|
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
|
||||||
$productBySlug = $slugQueryBuilder
|
|
||||||
->select('uid')
|
if ($productParam) {
|
||||||
->from('tx_vitec_domain_model_product')
|
if (!is_numeric($productParam)) {
|
||||||
->where(
|
$slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
$slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)),
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
$slugQueryBuilder->expr()->eq('deleted', 0),
|
|
||||||
$slugQueryBuilder->expr()->eq('hidden', 0)
|
$productBySlug = $slugQueryBuilder
|
||||||
)
|
->select('uid')
|
||||||
->executeQuery()
|
->from('tx_vitec_domain_model_product')
|
||||||
->fetchAssociative();
|
->where(
|
||||||
|
$slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)),
|
||||||
$productUid = (int)($productBySlug['uid'] ?? 0);
|
$slugQueryBuilder->expr()->eq('deleted', 0),
|
||||||
} else {
|
$slugQueryBuilder->expr()->eq('hidden', 0)
|
||||||
$productUid = (int)$productParam;
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
$productUid = (int)($productBySlug['uid'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$productUid = (int)$productParam;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!$productUid) {
|
||||||
if (!$productUid) {
|
return $debugMode
|
||||||
return json_encode([
|
? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]])
|
||||||
'error' => 'No product selected or found',
|
: '';
|
||||||
'debug' => [
|
}
|
||||||
'settings' => $settings,
|
|
||||||
'routeParams' => $routeParams ?? [],
|
$productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
'allQueryParams' => $GLOBALS['TYPO3_REQUEST']->getQueryParams() ?? [],
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
'requestUri' => $GLOBALS['TYPO3_REQUEST']->getUri()->getPath() ?? ''
|
|
||||||
]
|
$product = $productQueryBuilder
|
||||||
]);
|
->select('*')
|
||||||
}
|
->from('tx_vitec_domain_model_product')
|
||||||
|
->where(
|
||||||
// Query product
|
$productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
$productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$productQueryBuilder->expr()->eq('deleted', 0),
|
||||||
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
$productQueryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
$product = $productQueryBuilder
|
->executeQuery()
|
||||||
->select('*')
|
->fetchAssociative();
|
||||||
->from('tx_vitec_domain_model_product')
|
|
||||||
->where(
|
if (!$product) {
|
||||||
$productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
return $debugMode
|
||||||
$productQueryBuilder->expr()->eq('deleted', 0),
|
? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]])
|
||||||
$productQueryBuilder->expr()->eq('hidden', 0)
|
: '';
|
||||||
)
|
}
|
||||||
->executeQuery()
|
|
||||||
->fetchAssociative();
|
$response = [
|
||||||
|
'product' => $this->serializeProduct($product),
|
||||||
if (!$product) {
|
|
||||||
return json_encode([
|
|
||||||
'error' => 'Product not found',
|
|
||||||
'debug' => $debugMode ? ['productUid' => $productUid] : null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get product images
|
|
||||||
$images = $this->getProductImages((int)$product['uid']);
|
|
||||||
|
|
||||||
// Get categories
|
|
||||||
$categories = $this->getProductCategories((int)$product['uid']);
|
|
||||||
|
|
||||||
// Get downloads
|
|
||||||
$downloads = $this->getProductDownloads((int)$product['uid']);
|
|
||||||
|
|
||||||
// Build response
|
|
||||||
$response = [
|
|
||||||
'product' => [
|
|
||||||
'uid' => (int)$product['uid'],
|
|
||||||
'title' => $product['title'],
|
|
||||||
'subtitle' => $product['subtitle'],
|
|
||||||
'slug' => $product['slug'],
|
|
||||||
'teaser' => $product['teaser'],
|
|
||||||
'description' => $product['description'],
|
|
||||||
'seotitle' => $product['seotitle'],
|
|
||||||
'categories' => $categories,
|
|
||||||
'images' => $images,
|
|
||||||
'downloads' => $downloads,
|
|
||||||
],
|
|
||||||
'layout' => $layout,
|
|
||||||
'settings' => [
|
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
],
|
'settings' => [
|
||||||
];
|
'layout' => $layout,
|
||||||
|
],
|
||||||
if ($debugMode) {
|
|
||||||
$response['debug'] = [
|
|
||||||
'pageId' => $pageId,
|
|
||||||
'productUid' => $productUid,
|
|
||||||
'layout' => $layout,
|
|
||||||
'settings' => $settings,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'productUid' => $productUid,
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return json_encode($response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all images for a product with FAL and ImageService processing
|
* @param array<string,mixed> $product
|
||||||
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
|
protected function serializeProduct(array $product): array
|
||||||
|
{
|
||||||
|
$uid = (int)$product['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
|
||||||
|
'title' => (string)($product['title'] ?? ''),
|
||||||
|
'slug' => (string)($product['slug'] ?? ''),
|
||||||
|
'urltitle' => (string)($product['urltitle'] ?? ''),
|
||||||
|
'seotitle' => (string)($product['seotitle'] ?? ''),
|
||||||
|
'seometa' => (string)($product['seometa'] ?? ''),
|
||||||
|
'keywords' => (string)($product['keywords'] ?? ''),
|
||||||
|
'structureddata' => (string)($product['structureddata'] ?? ''),
|
||||||
|
'teaser' => (string)($product['teaser'] ?? ''),
|
||||||
|
'subtitle' => (string)($product['subtitle'] ?? ''),
|
||||||
|
'video' => (string)($product['video'] ?? ''),
|
||||||
|
'applications' => (string)($product['applications'] ?? ''),
|
||||||
|
'description' => (string)($product['description'] ?? ''),
|
||||||
|
'highlights' => (string)($product['highlights'] ?? ''),
|
||||||
|
'shortcutpid' => (string)($product['shortcutpid'] ?? ''),
|
||||||
|
'contentelement' => (string)($product['contentelement'] ?? ''),
|
||||||
|
'contentelementcta' => (string)($product['contentelementcta'] ?? ''),
|
||||||
|
|
||||||
|
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||||
|
'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false),
|
||||||
|
'hideonproducts' => (bool)($product['hideonproducts'] ?? false),
|
||||||
|
'shortcut' => (bool)($product['shortcut'] ?? false),
|
||||||
|
'legacy' => (bool)($product['legacy'] ?? false),
|
||||||
|
'supportproduct' => (bool)($product['supportproduct'] ?? false),
|
||||||
|
'subproduct' => (bool)($product['subproduct'] ?? false),
|
||||||
|
|
||||||
|
'link' => '/product/' . (string)($product['slug'] ?? ''),
|
||||||
|
|
||||||
|
'categories' => $this->getProductCategories($uid),
|
||||||
|
'images' => $this->getProductImages($uid),
|
||||||
|
'downloads' => $this->getProductDownloads($uid),
|
||||||
|
'ogimage' => $this->getProductOgImage($uid),
|
||||||
|
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected function getProductImages(int $productUid): array
|
protected function getProductImages(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_file_reference');
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
$fileReferences = $queryBuilder
|
$fileReferences = $queryBuilder
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('sys_file_reference')
|
->from('sys_file_reference')
|
||||||
@@ -191,27 +209,25 @@ class ProductShowJsonRenderer
|
|||||||
->orderBy('sorting_foreign', 'ASC')
|
->orderBy('sorting_foreign', 'ASC')
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
if (empty($fileReferences)) {
|
if (empty($fileReferences)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
$images = [];
|
$images = [];
|
||||||
|
|
||||||
foreach ($fileReferences as $fileRef) {
|
foreach ($fileReferences as $fileRef) {
|
||||||
try {
|
try {
|
||||||
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
||||||
$originalFile = $fileReference->getOriginalFile();
|
$originalFile = $fileReference->getOriginalFile();
|
||||||
|
|
||||||
// Process main image
|
|
||||||
$processedImage = $imageService->applyProcessingInstructions(
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => '1874c', 'height' => '625c']
|
['width' => '1874c', 'height' => '625c']
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate srcset
|
|
||||||
$srcset = [];
|
$srcset = [];
|
||||||
foreach ([400, 800, 1200, 1600] as $width) {
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
$processedVariant = $imageService->applyProcessingInstructions(
|
$processedVariant = $imageService->applyProcessingInstructions(
|
||||||
@@ -224,7 +240,7 @@ class ProductShowJsonRenderer
|
|||||||
'descriptor' => $width . 'w',
|
'descriptor' => $width . 'w',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$images[] = [
|
$images[] = [
|
||||||
'uid' => $fileRef['uid'],
|
'uid' => $fileRef['uid'],
|
||||||
'url' => $imageService->getImageUri($processedImage),
|
'url' => $imageService->getImageUri($processedImage),
|
||||||
@@ -239,22 +255,73 @@ class ProductShowJsonRenderer
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Skip invalid file references
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $images;
|
return $images;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get categories for a product
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
|
protected function getProductOgImage(int $productUid): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRef = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('sys_file_reference')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sorting_foreign', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
||||||
|
$originalFile = $fileReference->getOriginalFile();
|
||||||
|
|
||||||
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 1200]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $fileRef['uid'],
|
||||||
|
'url' => $imageService->getImageUri($processedImage),
|
||||||
|
'title' => $fileReference->getTitle() ?: '',
|
||||||
|
'alternative' => $fileReference->getAlternative() ?: '',
|
||||||
|
'description' => $fileReference->getDescription() ?: '',
|
||||||
|
'properties' => [
|
||||||
|
'width' => $originalFile->getProperty('width'),
|
||||||
|
'height' => $originalFile->getProperty('height'),
|
||||||
|
'mimeType' => $originalFile->getMimeType(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function getProductCategories(int $productUid): array
|
protected function getProductCategories(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_category');
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
$categories = $queryBuilder
|
$categories = $queryBuilder
|
||||||
->select('c.uid', 'c.title', 'c.description')
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
->from('sys_category', 'c')
|
->from('sys_category', 'c')
|
||||||
@@ -262,9 +329,9 @@ class ProductShowJsonRenderer
|
|||||||
'c',
|
'c',
|
||||||
'sys_category_record_mm',
|
'sys_category_record_mm',
|
||||||
'mm',
|
'mm',
|
||||||
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
$queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
|
||||||
' AND mm.fieldname = ' .
|
' AND mm.fieldname = ' .
|
||||||
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
)
|
)
|
||||||
->where(
|
->where(
|
||||||
@@ -275,7 +342,7 @@ class ProductShowJsonRenderer
|
|||||||
->orderBy('mm.sorting', 'ASC')
|
->orderBy('mm.sorting', 'ASC')
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
return array_map(function ($cat) {
|
return array_map(function ($cat) {
|
||||||
return [
|
return [
|
||||||
'uid' => (int)$cat['uid'],
|
'uid' => (int)$cat['uid'],
|
||||||
@@ -284,15 +351,12 @@ class ProductShowJsonRenderer
|
|||||||
];
|
];
|
||||||
}, $categories);
|
}, $categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all downloads for a product
|
|
||||||
*/
|
|
||||||
protected function getProductDownloads(int $productUid): array
|
protected function getProductDownloads(int $productUid): array
|
||||||
{
|
{
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('tx_vitec_domain_model_download');
|
->getQueryBuilderForTable('tx_vitec_domain_model_download');
|
||||||
|
|
||||||
$downloads = $queryBuilder
|
$downloads = $queryBuilder
|
||||||
->select('d.*')
|
->select('d.*')
|
||||||
->from('tx_vitec_domain_model_download', 'd')
|
->from('tx_vitec_domain_model_download', 'd')
|
||||||
@@ -311,16 +375,15 @@ class ProductShowJsonRenderer
|
|||||||
->orderBy('mm.sorting', 'ASC')
|
->orderBy('mm.sorting', 'ASC')
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAllAssociative();
|
->fetchAllAssociative();
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
foreach ($downloads as $download) {
|
foreach ($downloads as $download) {
|
||||||
$fileInfo = null;
|
$fileInfo = null;
|
||||||
|
|
||||||
// Get file information from FAL if file reference exists
|
|
||||||
if (!empty($download['file'])) {
|
if (!empty($download['file'])) {
|
||||||
$fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
->getQueryBuilderForTable('sys_file_reference');
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
$fileRef = $fileQueryBuilder
|
$fileRef = $fileQueryBuilder
|
||||||
->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type')
|
->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type')
|
||||||
->from('sys_file_reference', 'fr')
|
->from('sys_file_reference', 'fr')
|
||||||
@@ -336,7 +399,7 @@ class ProductShowJsonRenderer
|
|||||||
->setMaxResults(1)
|
->setMaxResults(1)
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
->fetchAssociative();
|
->fetchAssociative();
|
||||||
|
|
||||||
if ($fileRef) {
|
if ($fileRef) {
|
||||||
$fileInfo = [
|
$fileInfo = [
|
||||||
'uid' => (int)$fileRef['file_uid'],
|
'uid' => (int)$fileRef['file_uid'],
|
||||||
@@ -348,7 +411,7 @@ class ProductShowJsonRenderer
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'uid' => (int)$download['uid'],
|
'uid' => (int)$download['uid'],
|
||||||
'title' => $download['title'] ?? '',
|
'title' => $download['title'] ?? '',
|
||||||
@@ -360,7 +423,48 @@ class ProductShowJsonRenderer
|
|||||||
'file' => $fileInfo,
|
'file' => $fileInfo,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRelatedProducts(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$related = $queryBuilder
|
||||||
|
->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description')
|
||||||
|
->from('tx_vitec_domain_model_product', 'p')
|
||||||
|
->join(
|
||||||
|
'p',
|
||||||
|
'tx_vitec_product_related_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = p.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('p.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('p.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($related as $rel) {
|
||||||
|
$relUid = (int)$rel['uid'];
|
||||||
|
$result[] = [
|
||||||
|
'uid' => $relUid,
|
||||||
|
'title' => (string)($rel['title'] ?? ''),
|
||||||
|
'slug' => (string)($rel['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($rel['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($rel['teaser'] ?? ''),
|
||||||
|
'description' => (string)($rel['description'] ?? ''),
|
||||||
|
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
||||||
|
'images' => $this->getProductImages($relUid),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
366
packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244
Executable file
366
packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244
Executable file
@@ -0,0 +1,366 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||||
|
use TYPO3\CMS\Core\Resource\FileReference;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageService;
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render single product data as JSON for headless output
|
||||||
|
*/
|
||||||
|
class ProductShowJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$pageId = (int)$GLOBALS['TSFE']->id;
|
||||||
|
|
||||||
|
// DEBUG: Log that the UserFunc is being called
|
||||||
|
$debugInfo = [
|
||||||
|
'userFuncCalled' => true,
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'requestUri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Query tt_content for vitec_productshow on this page
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$debugInfo['contentElementsFound'] = count($contentElements);
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
$debugInfo['error'] = 'No vitec_productshow on page';
|
||||||
|
return json_encode(['debug' => $debugInfo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the first one
|
||||||
|
$contentElement = $contentElements[0];
|
||||||
|
|
||||||
|
// Parse FlexForm
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
// Get product UID from FlexForm or route parameter
|
||||||
|
$productUid = (int)($settings['product'] ?? 0);
|
||||||
|
$layout = (int)($settings['layout'] ?? 0);
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
// If no product selected in FlexForm, try to get from route parameter
|
||||||
|
if (!$productUid) {
|
||||||
|
// Get the product parameter from GET request
|
||||||
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
|
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
|
||||||
|
|
||||||
|
if ($productParam) {
|
||||||
|
// If it's a slug, resolve it to UID
|
||||||
|
if (!is_numeric($productParam)) {
|
||||||
|
$slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$productBySlug = $slugQueryBuilder
|
||||||
|
->select('uid')
|
||||||
|
->from('tx_vitec_domain_model_product')
|
||||||
|
->where(
|
||||||
|
$slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)),
|
||||||
|
$slugQueryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$slugQueryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
$productUid = (int)($productBySlug['uid'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$productUid = (int)$productParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$productUid) {
|
||||||
|
return json_encode([
|
||||||
|
'error' => 'No product selected or found',
|
||||||
|
'debug' => [
|
||||||
|
'settings' => $settings,
|
||||||
|
'routeParams' => $routeParams ?? [],
|
||||||
|
'allQueryParams' => $GLOBALS['TYPO3_REQUEST']->getQueryParams() ?? [],
|
||||||
|
'requestUri' => $GLOBALS['TYPO3_REQUEST']->getUri()->getPath() ?? ''
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query product
|
||||||
|
$productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$product = $productQueryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('tx_vitec_domain_model_product')
|
||||||
|
->where(
|
||||||
|
$productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$productQueryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return json_encode([
|
||||||
|
'error' => 'Product not found',
|
||||||
|
'debug' => $debugMode ? ['productUid' => $productUid] : null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get product images
|
||||||
|
$images = $this->getProductImages((int)$product['uid']);
|
||||||
|
|
||||||
|
// Get categories
|
||||||
|
$categories = $this->getProductCategories((int)$product['uid']);
|
||||||
|
|
||||||
|
// Get downloads
|
||||||
|
$downloads = $this->getProductDownloads((int)$product['uid']);
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
$response = [
|
||||||
|
'product' => [
|
||||||
|
'uid' => (int)$product['uid'],
|
||||||
|
'title' => $product['title'],
|
||||||
|
'subtitle' => $product['subtitle'],
|
||||||
|
'slug' => $product['slug'],
|
||||||
|
'teaser' => $product['teaser'],
|
||||||
|
'description' => $product['description'],
|
||||||
|
'seotitle' => $product['seotitle'],
|
||||||
|
'categories' => $categories,
|
||||||
|
'images' => $images,
|
||||||
|
'downloads' => $downloads,
|
||||||
|
],
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => [
|
||||||
|
'layout' => $layout,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'productUid' => $productUid,
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images for a product with FAL and ImageService processing
|
||||||
|
*/
|
||||||
|
protected function getProductImages(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileReferences = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('sys_file_reference')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sorting_foreign', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($fileReferences)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
foreach ($fileReferences as $fileRef) {
|
||||||
|
try {
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
||||||
|
$originalFile = $fileReference->getOriginalFile();
|
||||||
|
|
||||||
|
// Process main image
|
||||||
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => '1874c', 'height' => '625c']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate srcset
|
||||||
|
$srcset = [];
|
||||||
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
|
$processedVariant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c']
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($processedVariant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$images[] = [
|
||||||
|
'uid' => $fileRef['uid'],
|
||||||
|
'url' => $imageService->getImageUri($processedImage),
|
||||||
|
'title' => $fileReference->getTitle() ?: '',
|
||||||
|
'alternative' => $fileReference->getAlternative() ?: '',
|
||||||
|
'description' => $fileReference->getDescription() ?: '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $originalFile->getProperty('width'),
|
||||||
|
'height' => $originalFile->getProperty('height'),
|
||||||
|
'mimeType' => $originalFile->getMimeType(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Skip invalid file references
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $images;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories for a product
|
||||||
|
*/
|
||||||
|
protected function getProductCategories(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'],
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all downloads for a product
|
||||||
|
*/
|
||||||
|
protected function getProductDownloads(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_download');
|
||||||
|
|
||||||
|
$downloads = $queryBuilder
|
||||||
|
->select('d.*')
|
||||||
|
->from('tx_vitec_domain_model_download', 'd')
|
||||||
|
->join(
|
||||||
|
'd',
|
||||||
|
'tx_vitec_product_download_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = d.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('d.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hidden', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hideonwebsite', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($downloads as $download) {
|
||||||
|
$fileInfo = null;
|
||||||
|
|
||||||
|
// Get file information from FAL if file reference exists
|
||||||
|
if (!empty($download['file'])) {
|
||||||
|
$fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRef = $fileQueryBuilder
|
||||||
|
->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type')
|
||||||
|
->from('sys_file_reference', 'fr')
|
||||||
|
->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid')
|
||||||
|
->where(
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter((int)$download['uid'], ParameterType::INTEGER)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.tablenames', $fileQueryBuilder->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.fieldname', $fileQueryBuilder->createNamedParameter('file', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.deleted', 0),
|
||||||
|
$fileQueryBuilder->expr()->eq('f.missing', 0)
|
||||||
|
)
|
||||||
|
->orderBy('fr.sorting_foreign', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if ($fileRef) {
|
||||||
|
$fileInfo = [
|
||||||
|
'uid' => (int)$fileRef['file_uid'],
|
||||||
|
'name' => $fileRef['name'],
|
||||||
|
'url' => '/fileadmin' . $fileRef['identifier'],
|
||||||
|
'size' => (int)$fileRef['size'],
|
||||||
|
'extension' => $fileRef['extension'],
|
||||||
|
'mimeType' => $fileRef['mime_type'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'uid' => (int)$download['uid'],
|
||||||
|
'title' => $download['title'] ?? '',
|
||||||
|
'slug' => $download['slug'] ?? '',
|
||||||
|
'teaser' => $download['teaser'] ?? '',
|
||||||
|
'description' => $download['description'] ?? '',
|
||||||
|
'keywords' => $download['keywords'] ?? '',
|
||||||
|
'icon' => $download['icon'] ?? '',
|
||||||
|
'file' => $fileInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
512
packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048
Executable file
512
packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048
Executable file
@@ -0,0 +1,512 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||||
|
use TYPO3\CMS\Core\Resource\FileReference;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageService;
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render single product data as JSON for headless output
|
||||||
|
*/
|
||||||
|
class ProductShowJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$pageId = (int)$GLOBALS['TSFE']->id;
|
||||||
|
|
||||||
|
// DEBUG: Log that the UserFunc is being called
|
||||||
|
$debugInfo = [
|
||||||
|
'userFuncCalled' => true,
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'requestUri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Query tt_content for vitec_productshow on this page
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$debugInfo['contentElementsFound'] = count($contentElements);
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
$debugInfo['error'] = 'No vitec_productshow on page';
|
||||||
|
return json_encode(['debug' => $debugInfo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the first one
|
||||||
|
$contentElement = $contentElements[0];
|
||||||
|
|
||||||
|
// Parse FlexForm
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
// Get product UID from FlexForm or route parameter
|
||||||
|
$productUid = (int)($settings['product'] ?? 0);
|
||||||
|
$layout = (int)($settings['layout'] ?? 0);
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
// If no product selected in FlexForm, try to get from route parameter
|
||||||
|
if (!$productUid) {
|
||||||
|
// Get the product parameter from GET request
|
||||||
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
|
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
|
||||||
|
|
||||||
|
if ($productParam) {
|
||||||
|
// If it's a slug, resolve it to UID
|
||||||
|
if (!is_numeric($productParam)) {
|
||||||
|
$slugQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$productBySlug = $slugQueryBuilder
|
||||||
|
->select('uid')
|
||||||
|
->from('tx_vitec_domain_model_product')
|
||||||
|
->where(
|
||||||
|
$slugQueryBuilder->expr()->eq('slug', $slugQueryBuilder->createNamedParameter($productParam)),
|
||||||
|
$slugQueryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$slugQueryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
$productUid = (int)($productBySlug['uid'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$productUid = (int)$productParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$productUid) {
|
||||||
|
return json_encode([
|
||||||
|
'error' => 'No product selected or found',
|
||||||
|
'debug' => [
|
||||||
|
'settings' => $settings,
|
||||||
|
'routeParams' => $routeParams ?? [],
|
||||||
|
'allQueryParams' => $GLOBALS['TYPO3_REQUEST']->getQueryParams() ?? [],
|
||||||
|
'requestUri' => $GLOBALS['TYPO3_REQUEST']->getUri()->getPath() ?? ''
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query product
|
||||||
|
$productQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$product = $productQueryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('tx_vitec_domain_model_product')
|
||||||
|
->where(
|
||||||
|
$productQueryBuilder->expr()->eq('uid', $productQueryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$productQueryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$productQueryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return json_encode([
|
||||||
|
'error' => 'Product not found',
|
||||||
|
'debug' => $debugMode ? ['productUid' => $productUid] : null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response (full Product domain model + resolved relations)
|
||||||
|
$response = [
|
||||||
|
'product' => $this->serializeProduct($product),
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => [
|
||||||
|
'layout' => $layout,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'productUid' => $productUid,
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a single product DB row to the full headless JSON structure.
|
||||||
|
*
|
||||||
|
* Includes every field declared on the Product domain model
|
||||||
|
* (Evomedien\Vitec\Domain\Model\Product) plus all fully resolved
|
||||||
|
* relations (categories, productimage, downloads, ogimage, relatedprodukt).
|
||||||
|
*
|
||||||
|
* Kept structurally identical to ProductListJsonRenderer::serializeProduct()
|
||||||
|
* so list and detail endpoints expose the same product schema.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $product Associative DB row of tx_vitec_domain_model_product
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeProduct(array $product): array
|
||||||
|
{
|
||||||
|
$uid = (int)$product['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
// --- identifier ---
|
||||||
|
'uid' => $uid,
|
||||||
|
|
||||||
|
// --- scalar string fields (Product domain model) ---
|
||||||
|
'title' => (string)($product['title'] ?? ''),
|
||||||
|
'slug' => (string)($product['slug'] ?? ''),
|
||||||
|
'urltitle' => (string)($product['urltitle'] ?? ''),
|
||||||
|
'seotitle' => (string)($product['seotitle'] ?? ''),
|
||||||
|
'seometa' => (string)($product['seometa'] ?? ''),
|
||||||
|
'keywords' => (string)($product['keywords'] ?? ''),
|
||||||
|
'structureddata' => (string)($product['structureddata'] ?? ''),
|
||||||
|
'teaser' => (string)($product['teaser'] ?? ''),
|
||||||
|
'subtitle' => (string)($product['subtitle'] ?? ''),
|
||||||
|
'video' => (string)($product['video'] ?? ''),
|
||||||
|
'applications' => (string)($product['applications'] ?? ''),
|
||||||
|
'description' => (string)($product['description'] ?? ''),
|
||||||
|
'highlights' => (string)($product['highlights'] ?? ''),
|
||||||
|
'shortcutpid' => (string)($product['shortcutpid'] ?? ''),
|
||||||
|
'contentelement' => (string)($product['contentelement'] ?? ''),
|
||||||
|
'contentelementcta' => (string)($product['contentelementcta'] ?? ''),
|
||||||
|
|
||||||
|
// --- boolean flags (Product domain model) ---
|
||||||
|
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||||
|
'hideondatasheets' => (bool)($product['hideondatasheets'] ?? false),
|
||||||
|
'hideonproducts' => (bool)($product['hideonproducts'] ?? false),
|
||||||
|
'shortcut' => (bool)($product['shortcut'] ?? false),
|
||||||
|
'legacy' => (bool)($product['legacy'] ?? false),
|
||||||
|
'supportproduct' => (bool)($product['supportproduct'] ?? false),
|
||||||
|
'subproduct' => (bool)($product['subproduct'] ?? false),
|
||||||
|
|
||||||
|
// --- convenience link (kept for backward compatibility) ---
|
||||||
|
'link' => '/product/' . (string)($product['slug'] ?? ''),
|
||||||
|
|
||||||
|
// --- fully resolved relations ---
|
||||||
|
'categories' => $this->getProductCategories($uid),
|
||||||
|
'images' => $this->getProductImages($uid),
|
||||||
|
'downloads' => $this->getProductDownloads($uid),
|
||||||
|
'ogimage' => $this->getProductOgImage($uid),
|
||||||
|
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images for a product with FAL and ImageService processing
|
||||||
|
*/
|
||||||
|
protected function getProductImages(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileReferences = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('sys_file_reference')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sorting_foreign', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($fileReferences)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
foreach ($fileReferences as $fileRef) {
|
||||||
|
try {
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
||||||
|
$originalFile = $fileReference->getOriginalFile();
|
||||||
|
|
||||||
|
// Process main image
|
||||||
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => '1874c', 'height' => '625c']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate srcset
|
||||||
|
$srcset = [];
|
||||||
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
|
$processedVariant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c']
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($processedVariant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$images[] = [
|
||||||
|
'uid' => $fileRef['uid'],
|
||||||
|
'url' => $imageService->getImageUri($processedImage),
|
||||||
|
'title' => $fileReference->getTitle() ?: '',
|
||||||
|
'alternative' => $fileReference->getAlternative() ?: '',
|
||||||
|
'description' => $fileReference->getDescription() ?: '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $originalFile->getProperty('width'),
|
||||||
|
'height' => $originalFile->getProperty('height'),
|
||||||
|
'mimeType' => $originalFile->getMimeType(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Skip invalid file references
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $images;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the single Open Graph image (ogimage) for a product, or null.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getProductOgImage(int $productUid): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRef = $queryBuilder
|
||||||
|
->select('*')
|
||||||
|
->from('sys_file_reference')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('ogimage', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sorting_foreign', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject($fileRef['uid']);
|
||||||
|
$originalFile = $fileReference->getOriginalFile();
|
||||||
|
|
||||||
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 1200]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $fileRef['uid'],
|
||||||
|
'url' => $imageService->getImageUri($processedImage),
|
||||||
|
'title' => $fileReference->getTitle() ?: '',
|
||||||
|
'alternative' => $fileReference->getAlternative() ?: '',
|
||||||
|
'description' => $fileReference->getDescription() ?: '',
|
||||||
|
'properties' => [
|
||||||
|
'width' => $originalFile->getProperty('width'),
|
||||||
|
'height' => $originalFile->getProperty('height'),
|
||||||
|
'mimeType' => $originalFile->getMimeType(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories for a product
|
||||||
|
*/
|
||||||
|
protected function getProductCategories(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'],
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all downloads for a product
|
||||||
|
*/
|
||||||
|
protected function getProductDownloads(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_download');
|
||||||
|
|
||||||
|
$downloads = $queryBuilder
|
||||||
|
->select('d.*')
|
||||||
|
->from('tx_vitec_domain_model_download', 'd')
|
||||||
|
->join(
|
||||||
|
'd',
|
||||||
|
'tx_vitec_product_download_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = d.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('d.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hidden', 0),
|
||||||
|
$queryBuilder->expr()->eq('d.hideonwebsite', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($downloads as $download) {
|
||||||
|
$fileInfo = null;
|
||||||
|
|
||||||
|
// Get file information from FAL if file reference exists
|
||||||
|
if (!empty($download['file'])) {
|
||||||
|
$fileQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRef = $fileQueryBuilder
|
||||||
|
->select('fr.uid', 'f.uid as file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type')
|
||||||
|
->from('sys_file_reference', 'fr')
|
||||||
|
->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid')
|
||||||
|
->where(
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.uid_foreign', $fileQueryBuilder->createNamedParameter((int)$download['uid'], ParameterType::INTEGER)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.tablenames', $fileQueryBuilder->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.fieldname', $fileQueryBuilder->createNamedParameter('file', ParameterType::STRING)),
|
||||||
|
$fileQueryBuilder->expr()->eq('fr.deleted', 0),
|
||||||
|
$fileQueryBuilder->expr()->eq('f.missing', 0)
|
||||||
|
)
|
||||||
|
->orderBy('fr.sorting_foreign', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if ($fileRef) {
|
||||||
|
$fileInfo = [
|
||||||
|
'uid' => (int)$fileRef['file_uid'],
|
||||||
|
'name' => $fileRef['name'],
|
||||||
|
'url' => '/fileadmin' . $fileRef['identifier'],
|
||||||
|
'size' => (int)$fileRef['size'],
|
||||||
|
'extension' => $fileRef['extension'],
|
||||||
|
'mimeType' => $fileRef['mime_type'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'uid' => (int)$download['uid'],
|
||||||
|
'title' => $download['title'] ?? '',
|
||||||
|
'slug' => $download['slug'] ?? '',
|
||||||
|
'teaser' => $download['teaser'] ?? '',
|
||||||
|
'description' => $download['description'] ?? '',
|
||||||
|
'keywords' => $download['keywords'] ?? '',
|
||||||
|
'icon' => $download['icon'] ?? '',
|
||||||
|
'file' => $fileInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get related products (resolved via tx_vitec_product_related_mm).
|
||||||
|
*
|
||||||
|
* Returns a shallow representation (no nested relations) to avoid
|
||||||
|
* infinite recursion between mutually related products.
|
||||||
|
*/
|
||||||
|
protected function getRelatedProducts(int $productUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_product');
|
||||||
|
|
||||||
|
$related = $queryBuilder
|
||||||
|
->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description')
|
||||||
|
->from('tx_vitec_domain_model_product', 'p')
|
||||||
|
->join(
|
||||||
|
'p',
|
||||||
|
'tx_vitec_product_related_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_foreign = p.uid'
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('p.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('p.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($related as $rel) {
|
||||||
|
$relUid = (int)$rel['uid'];
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'uid' => $relUid,
|
||||||
|
'title' => (string)($rel['title'] ?? ''),
|
||||||
|
'slug' => (string)($rel['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($rel['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($rel['teaser'] ?? ''),
|
||||||
|
'description' => (string)($rel['description'] ?? ''),
|
||||||
|
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
||||||
|
'images' => $this->getProductImages($relUid),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
223
packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php
Executable file
223
packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php
Executable file
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render the usecase list as JSON for headless output.
|
||||||
|
*
|
||||||
|
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
|
||||||
|
* one specific tt_content row, reused by ContainerChildrenProcessor for
|
||||||
|
* usecase-list plugins nested in a b13 container. Exception-safe.
|
||||||
|
*/
|
||||||
|
class UsecaseListJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $ttContentQb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)),
|
||||||
|
$ttContentQb->expr()->eq('deleted', 0),
|
||||||
|
$ttContentQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForRecord($contentElements[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'] ?? [];
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
|
||||||
|
$usecases = $usecaseQb
|
||||||
|
->select('u.*')
|
||||||
|
->from('tx_vitec_domain_model_usecase', 'u')
|
||||||
|
->where(
|
||||||
|
$usecaseQb->expr()->eq('u.deleted', 0),
|
||||||
|
$usecaseQb->expr()->eq('u.hidden', 0),
|
||||||
|
$usecaseQb->expr()->eq('u.hideonwebsite', 0)
|
||||||
|
)
|
||||||
|
->orderBy('u.title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$usecasesData = [];
|
||||||
|
foreach ($usecases as $usecase) {
|
||||||
|
$usecasesData[] = $this->serializeUsecase($usecase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
return json_encode([
|
||||||
|
'usecases' => $usecasesData,
|
||||||
|
'debug' => [
|
||||||
|
'pageId' => (int)($GLOBALS['TSFE']->id ?? 0),
|
||||||
|
'usecaseCount' => count($usecasesData),
|
||||||
|
'settings' => $settings,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($usecasesData);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $usecase
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeUsecase(array $usecase): array
|
||||||
|
{
|
||||||
|
$uid = (int)$usecase['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
'title' => (string)($usecase['title'] ?? ''),
|
||||||
|
'slug' => (string)($usecase['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($usecase['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($usecase['teaser'] ?? ''),
|
||||||
|
'description' => (string)($usecase['description'] ?? ''),
|
||||||
|
'singlepid' => (string)($usecase['singlepid'] ?? ''),
|
||||||
|
'hideonapp' => (bool)($usecase['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false),
|
||||||
|
'categories' => $this->getUsecaseCategories($uid),
|
||||||
|
'caseimage' => $this->getUsecaseImage($uid, 'caseimage'),
|
||||||
|
'logoimage' => $this->getUsecaseImage($uid, 'logoimage'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRefData = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRefData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
$srcset = [];
|
||||||
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($variant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($default),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUsecaseCategories(int $usecaseUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(static function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'] ?? '',
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
244
packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048
Executable file
244
packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048
Executable file
@@ -0,0 +1,244 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render the usecase list as JSON for headless output.
|
||||||
|
*
|
||||||
|
* Mirrors ProductListJsonRenderer: Extbase repositories don't work in a
|
||||||
|
* UserFunc context (framework not bootstrapped), so tt_content and the
|
||||||
|
* usecase records are queried directly. Exception-safe: returns an empty
|
||||||
|
* string on any failure so headless `ifEmptyUnsetKey` can drop the key.
|
||||||
|
*/
|
||||||
|
class UsecaseListJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
// Find the vitec_usecaselist plugin on this page
|
||||||
|
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $ttContentQb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaselist', ParameterType::STRING)),
|
||||||
|
$ttContentQb->expr()->eq('deleted', 0),
|
||||||
|
$ttContentQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
// Not a usecase-list page (or none configured): emit nothing
|
||||||
|
// so headless removes the key entirely.
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentElement = $contentElements[0];
|
||||||
|
|
||||||
|
// Parse FlexForm (debug flag only; the list shows all visible usecases)
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
// Query visible usecases directly (Extbase unavailable in UserFunc)
|
||||||
|
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
|
||||||
|
$usecases = $usecaseQb
|
||||||
|
->select('u.*')
|
||||||
|
->from('tx_vitec_domain_model_usecase', 'u')
|
||||||
|
->where(
|
||||||
|
$usecaseQb->expr()->eq('u.deleted', 0),
|
||||||
|
$usecaseQb->expr()->eq('u.hidden', 0),
|
||||||
|
$usecaseQb->expr()->eq('u.hideonwebsite', 0)
|
||||||
|
)
|
||||||
|
->orderBy('u.title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$usecasesData = [];
|
||||||
|
foreach ($usecases as $usecase) {
|
||||||
|
$usecasesData[] = $this->serializeUsecase($usecase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
return json_encode([
|
||||||
|
'usecases' => $usecasesData,
|
||||||
|
'debug' => [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'usecaseCount' => count($usecasesData),
|
||||||
|
'settings' => $settings,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($usecasesData);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a single usecase DB row to the full headless JSON structure.
|
||||||
|
*
|
||||||
|
* Includes every field declared on the Usecase domain model
|
||||||
|
* (Evomedien\Vitec\Domain\Model\Usecase) plus all resolved relations
|
||||||
|
* (categories, caseimage, logoimage). Kept structurally analogous to
|
||||||
|
* ProductListJsonRenderer::serializeProduct().
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $usecase
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeUsecase(array $usecase): array
|
||||||
|
{
|
||||||
|
$uid = (int)$usecase['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
// --- identifier ---
|
||||||
|
'uid' => $uid,
|
||||||
|
|
||||||
|
// --- scalar string fields (Usecase domain model) ---
|
||||||
|
'title' => (string)($usecase['title'] ?? ''),
|
||||||
|
'slug' => (string)($usecase['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($usecase['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($usecase['teaser'] ?? ''),
|
||||||
|
'description' => (string)($usecase['description'] ?? ''),
|
||||||
|
'singlepid' => (string)($usecase['singlepid'] ?? ''),
|
||||||
|
|
||||||
|
// --- boolean flags (Usecase domain model) ---
|
||||||
|
'hideonapp' => (bool)($usecase['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false),
|
||||||
|
|
||||||
|
// --- fully resolved relations ---
|
||||||
|
'categories' => $this->getUsecaseCategories($uid),
|
||||||
|
'caseimage' => $this->getUsecaseImage($uid, 'caseimage'),
|
||||||
|
'logoimage' => $this->getUsecaseImage($uid, 'logoimage'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a single FAL image field (caseimage / logoimage) with srcset.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRefData = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRefData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
$sizes = [400, 800, 1200, 1600];
|
||||||
|
$srcset = [];
|
||||||
|
foreach ($sizes as $width) {
|
||||||
|
$processedVariant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($processedVariant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($default),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories for a usecase (resolved sys_category records).
|
||||||
|
*/
|
||||||
|
protected function getUsecaseCategories(int $usecaseUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(static function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'] ?? '',
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
265
packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php
Executable file
265
packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php
Executable file
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render a single usecase as JSON for headless output.
|
||||||
|
*
|
||||||
|
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
|
||||||
|
* one specific tt_content row, reused by ContainerChildrenProcessor for
|
||||||
|
* usecase-show plugins nested in a b13 container. Exception-safe.
|
||||||
|
*/
|
||||||
|
class UsecaseShowJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $ttContentQb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)),
|
||||||
|
$ttContentQb->expr()->eq('deleted', 0),
|
||||||
|
$ttContentQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForRecord($contentElements[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $contentElement
|
||||||
|
*/
|
||||||
|
public function renderForRecord(array $contentElement): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
$usecaseUid = (int)($settings['usecase'] ?? 0);
|
||||||
|
$layout = (string)($settings['layout'] ?? 'default');
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
if (!$usecaseUid) {
|
||||||
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
|
$usecaseParam = $routeParams['tx_vitec_usecaseshow']['usecase'] ?? null;
|
||||||
|
|
||||||
|
if ($usecaseParam) {
|
||||||
|
if (!is_numeric($usecaseParam)) {
|
||||||
|
$slugQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
$bySlug = $slugQb
|
||||||
|
->select('uid')
|
||||||
|
->from('tx_vitec_domain_model_usecase')
|
||||||
|
->where(
|
||||||
|
$slugQb->expr()->eq('slug', $slugQb->createNamedParameter($usecaseParam)),
|
||||||
|
$slugQb->expr()->eq('deleted', 0),
|
||||||
|
$slugQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
$usecaseUid = (int)($bySlug['uid'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$usecaseUid = (int)$usecaseParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$usecaseUid) {
|
||||||
|
return $debugMode
|
||||||
|
? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]])
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
|
||||||
|
$usecase = $usecaseQb
|
||||||
|
->select('*')
|
||||||
|
->from('tx_vitec_domain_model_usecase')
|
||||||
|
->where(
|
||||||
|
$usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$usecaseQb->expr()->eq('deleted', 0),
|
||||||
|
$usecaseQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$usecase) {
|
||||||
|
return $debugMode
|
||||||
|
? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]])
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'usecase' => $this->serializeUsecase($usecase),
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => [
|
||||||
|
'layout' => $layout,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'usecaseUid' => $usecaseUid,
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $usecase
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeUsecase(array $usecase): array
|
||||||
|
{
|
||||||
|
$uid = (int)$usecase['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
'title' => (string)($usecase['title'] ?? ''),
|
||||||
|
'slug' => (string)($usecase['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($usecase['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($usecase['teaser'] ?? ''),
|
||||||
|
'description' => (string)($usecase['description'] ?? ''),
|
||||||
|
'singlepid' => (string)($usecase['singlepid'] ?? ''),
|
||||||
|
'hideonapp' => (bool)($usecase['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false),
|
||||||
|
'categories' => $this->getUsecaseCategories($uid),
|
||||||
|
'caseimage' => $this->getUsecaseImage($uid, 'caseimage'),
|
||||||
|
'logoimage' => $this->getUsecaseImage($uid, 'logoimage'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRefData = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRefData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
$srcset = [];
|
||||||
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($variant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($default),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUsecaseCategories(int $usecaseUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(static function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'] ?? '',
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
271
packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048
Executable file
271
packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048
Executable file
@@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc to render a single usecase as JSON for headless output.
|
||||||
|
*
|
||||||
|
* Mirrors ProductShowJsonRenderer. Extbase repositories are unavailable in a
|
||||||
|
* UserFunc context, so tt_content and the usecase record are queried directly.
|
||||||
|
* Exception-safe: returns '' on any failure / when not applicable so headless
|
||||||
|
* `ifEmptyUnsetKey` drops the key entirely.
|
||||||
|
*/
|
||||||
|
class UsecaseShowJsonRenderer
|
||||||
|
{
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
|
||||||
|
// Find the vitec_usecaseshow plugin on this page
|
||||||
|
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tt_content');
|
||||||
|
|
||||||
|
$contentElements = $ttContentQb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$ttContentQb->expr()->eq('pid', $ttContentQb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$ttContentQb->expr()->eq('list_type', $ttContentQb->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)),
|
||||||
|
$ttContentQb->expr()->eq('deleted', 0),
|
||||||
|
$ttContentQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($contentElements)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentElement = $contentElements[0];
|
||||||
|
|
||||||
|
// Parse FlexForm
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
$usecaseUid = (int)($settings['usecase'] ?? 0);
|
||||||
|
$layout = (string)($settings['layout'] ?? 'default');
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
// Fall back to route / query parameter (uid or slug)
|
||||||
|
if (!$usecaseUid) {
|
||||||
|
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
|
||||||
|
$usecaseParam = $routeParams['tx_vitec_usecaseshow']['usecase'] ?? null;
|
||||||
|
|
||||||
|
if ($usecaseParam) {
|
||||||
|
if (!is_numeric($usecaseParam)) {
|
||||||
|
$slugQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
$bySlug = $slugQb
|
||||||
|
->select('uid')
|
||||||
|
->from('tx_vitec_domain_model_usecase')
|
||||||
|
->where(
|
||||||
|
$slugQb->expr()->eq('slug', $slugQb->createNamedParameter($usecaseParam)),
|
||||||
|
$slugQb->expr()->eq('deleted', 0),
|
||||||
|
$slugQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
$usecaseUid = (int)($bySlug['uid'] ?? 0);
|
||||||
|
} else {
|
||||||
|
$usecaseUid = (int)$usecaseParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$usecaseUid) {
|
||||||
|
return $debugMode
|
||||||
|
? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]])
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the usecase
|
||||||
|
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||||
|
|
||||||
|
$usecase = $usecaseQb
|
||||||
|
->select('*')
|
||||||
|
->from('tx_vitec_domain_model_usecase')
|
||||||
|
->where(
|
||||||
|
$usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$usecaseQb->expr()->eq('deleted', 0),
|
||||||
|
$usecaseQb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$usecase) {
|
||||||
|
return $debugMode
|
||||||
|
? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]])
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'usecase' => $this->serializeUsecase($usecase),
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => [
|
||||||
|
'layout' => $layout,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'usecaseUid' => $usecaseUid,
|
||||||
|
'layout' => $layout,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a single usecase DB row. Structurally identical to
|
||||||
|
* UsecaseListJsonRenderer::serializeUsecase() so list and detail
|
||||||
|
* endpoints expose the same usecase schema.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $usecase
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected function serializeUsecase(array $usecase): array
|
||||||
|
{
|
||||||
|
$uid = (int)$usecase['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
'title' => (string)($usecase['title'] ?? ''),
|
||||||
|
'slug' => (string)($usecase['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($usecase['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($usecase['teaser'] ?? ''),
|
||||||
|
'description' => (string)($usecase['description'] ?? ''),
|
||||||
|
'singlepid' => (string)($usecase['singlepid'] ?? ''),
|
||||||
|
'hideonapp' => (bool)($usecase['hideonapp'] ?? false),
|
||||||
|
'hideonwebsite' => (bool)($usecase['hideonwebsite'] ?? false),
|
||||||
|
'categories' => $this->getUsecaseCategories($uid),
|
||||||
|
'caseimage' => $this->getUsecaseImage($uid, 'caseimage'),
|
||||||
|
'logoimage' => $this->getUsecaseImage($uid, 'logoimage'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a single FAL image field (caseimage / logoimage) with srcset.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_file_reference');
|
||||||
|
|
||||||
|
$fileRefData = $queryBuilder
|
||||||
|
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
|
||||||
|
->from('sys_file_reference', 'sfr')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('sfr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('sfr.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('sfr.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('sfr.sorting_foreign')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (!$fileRefData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||||
|
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||||
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
|
||||||
|
|
||||||
|
$srcset = [];
|
||||||
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($variant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => (int)$fileRefData['uid'],
|
||||||
|
'url' => $imageService->getImageUri($default),
|
||||||
|
'title' => $fileRefData['title'] ?? '',
|
||||||
|
'alternative' => $fileRefData['alternative'] ?? '',
|
||||||
|
'description' => $fileRefData['description'] ?? '',
|
||||||
|
'srcset' => $srcset,
|
||||||
|
'properties' => [
|
||||||
|
'width' => $fileReference->getProperty('width'),
|
||||||
|
'height' => $fileReference->getProperty('height'),
|
||||||
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories for a usecase (resolved sys_category records).
|
||||||
|
*/
|
||||||
|
protected function getUsecaseCategories(int $usecaseUid): array
|
||||||
|
{
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('sys_category');
|
||||||
|
|
||||||
|
$categories = $queryBuilder
|
||||||
|
->select('c.uid', 'c.title', 'c.description')
|
||||||
|
->from('sys_category', 'c')
|
||||||
|
->join(
|
||||||
|
'c',
|
||||||
|
'sys_category_record_mm',
|
||||||
|
'mm',
|
||||||
|
'mm.uid_local = c.uid AND mm.tablenames = ' .
|
||||||
|
$queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) .
|
||||||
|
' AND mm.fieldname = ' .
|
||||||
|
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
|
||||||
|
)
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
|
||||||
|
$queryBuilder->expr()->eq('c.deleted', 0),
|
||||||
|
$queryBuilder->expr()->eq('c.hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('mm.sorting', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
return array_map(static function ($cat) {
|
||||||
|
return [
|
||||||
|
'uid' => (int)$cat['uid'],
|
||||||
|
'title' => $cat['title'] ?? '',
|
||||||
|
'description' => $cat['description'] ?? '',
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/vitec/Configuration/Backend/Modules.php
Normal file
34
packages/vitec/Configuration/Backend/Modules.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Controller\OgImageController;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'web_vitecogimage' => [
|
||||||
|
'parent' => 'web',
|
||||||
|
'position' => ['after' => 'web_info'],
|
||||||
|
'access' => 'user',
|
||||||
|
'workspaces' => 'live',
|
||||||
|
'path' => '/module/web/vitec-ogimage',
|
||||||
|
'labels' => [
|
||||||
|
'title' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf:mlang_tabs_tab',
|
||||||
|
'shortDescription' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf:mlang_labels_tabdescr',
|
||||||
|
],
|
||||||
|
'extensionName' => 'Vitec',
|
||||||
|
'iconIdentifier' => 'vitec-ogimage',
|
||||||
|
'routes' => [
|
||||||
|
'_default' => [
|
||||||
|
'target' => OgImageController::class . '::indexAction',
|
||||||
|
],
|
||||||
|
'generate' => [
|
||||||
|
'target' => OgImageController::class . '::generateAction',
|
||||||
|
'methods' => ['POST'],
|
||||||
|
],
|
||||||
|
'preview' => [
|
||||||
|
'target' => OgImageController::class . '::previewAction',
|
||||||
|
'methods' => ['POST'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
22
packages/vitec/Configuration/FlexForms/Container.xml
Executable file
22
packages/vitec/Configuration/FlexForms/Container.xml
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.sheet</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.cssClass>
|
||||||
|
<label>LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.cssclass.label</label>
|
||||||
|
<description>LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.cssclass.description</description>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>30</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
</config>
|
||||||
|
</settings.cssClass>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
@@ -3,27 +3,22 @@
|
|||||||
<sheets>
|
<sheets>
|
||||||
<sDEF>
|
<sDEF>
|
||||||
<ROOT>
|
<ROOT>
|
||||||
<sheetTitle>
|
<sheetTitle>Usecase List</sheetTitle>
|
||||||
Select Usecase
|
|
||||||
</sheetTitle>
|
|
||||||
<type>array</type>
|
<type>array</type>
|
||||||
<el>
|
<el>
|
||||||
<settings.usecase>
|
<settings.debug>
|
||||||
<label>
|
<label>Allow Debug Output.</label>
|
||||||
Usecase
|
|
||||||
</label>
|
|
||||||
<config>
|
<config>
|
||||||
<type>select</type>
|
<type>check</type>
|
||||||
<renderType>selectSingle</renderType>
|
<items type="array">
|
||||||
<foreign_table>tx_vitec_domain_model_usecase</foreign_table>
|
<numIndex index="0" type="array">
|
||||||
<foreign_table_where>AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title</foreign_table_where>
|
<label>LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled</label>
|
||||||
<size>1</size>
|
</numIndex>
|
||||||
<minitems>0</minitems>
|
</items>
|
||||||
<maxitems>1</maxitems>
|
|
||||||
</config>
|
</config>
|
||||||
</settings.usecase>
|
</settings.debug>
|
||||||
</el>
|
</el>
|
||||||
</ROOT>
|
</ROOT>
|
||||||
</sDEF>
|
</sDEF>
|
||||||
</sheets>
|
</sheets>
|
||||||
</T3DataStructure>
|
</T3DataStructure>
|
||||||
|
|||||||
29
packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402
Executable file
29
packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>
|
||||||
|
Select Usecase
|
||||||
|
</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.usecase>
|
||||||
|
<label>
|
||||||
|
Usecase
|
||||||
|
</label>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<foreign_table>tx_vitec_domain_model_usecase</foreign_table>
|
||||||
|
<foreign_table_where>AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title</foreign_table_where>
|
||||||
|
<size>1</size>
|
||||||
|
<minitems>0</minitems>
|
||||||
|
<maxitems>1</maxitems>
|
||||||
|
</config>
|
||||||
|
</settings.usecase>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
@@ -24,4 +24,8 @@ return [
|
|||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg',
|
||||||
],
|
],
|
||||||
|
'vitec-ogimage' => [
|
||||||
|
'provider' => SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,28 +8,82 @@ config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Headless: Add products field to vitec_productlist
|
# =============================================================================
|
||||||
|
# Headless list-plugin JSON renderers
|
||||||
|
#
|
||||||
|
# `tt_content.list.20.<list_type>` renders the element body for that subtype
|
||||||
|
# (only ever invoked for the matching list_type — safe as-is).
|
||||||
|
#
|
||||||
|
# `tt_content.list.fields.content.fields.<key>` is GLOBAL: without scoping it
|
||||||
|
# would inject <key> into EVERY list plugin's content. Each such field is
|
||||||
|
# therefore guarded with:
|
||||||
|
# stdWrap.if { equals.field = list_type ... } -> empty for other plugins
|
||||||
|
# ifEmptyUnsetKey = 1 -> empty key removed entirely
|
||||||
|
# (headless JsonContentObject honours ifEmptyUnsetKey: '' / false => unset).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- vitec_productlist --------------------------------------------------------
|
||||||
tt_content.list.20.vitec_productlist = USER
|
tt_content.list.20.vitec_productlist = USER
|
||||||
tt_content.list.20.vitec_productlist {
|
tt_content.list.20.vitec_productlist {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
}
|
}
|
||||||
|
|
||||||
# Override the JSON structure for vitec_productlist
|
|
||||||
tt_content.list.fields.content.fields.products = USER
|
tt_content.list.fields.content.fields.products = USER
|
||||||
tt_content.list.fields.content.fields.products {
|
tt_content.list.fields.content.fields.products {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_productlist
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Headless: Add product field to vitec_productshow
|
# --- vitec_productshow --------------------------------------------------------
|
||||||
tt_content.list.20.vitec_productshow = USER
|
tt_content.list.20.vitec_productshow = USER
|
||||||
tt_content.list.20.vitec_productshow {
|
tt_content.list.20.vitec_productshow {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
}
|
}
|
||||||
|
|
||||||
# Override the JSON structure for vitec_productshow
|
|
||||||
tt_content.list.fields.content.fields.product = USER
|
tt_content.list.fields.content.fields.product = USER
|
||||||
tt_content.list.fields.content.fields.product {
|
tt_content.list.fields.content.fields.product {
|
||||||
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_productshow
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vitec_usecaselist --------------------------------------------------------
|
||||||
|
tt_content.list.20.vitec_usecaselist = USER
|
||||||
|
tt_content.list.20.vitec_usecaselist {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.list.fields.content.fields.usecases = USER
|
||||||
|
tt_content.list.fields.content.fields.usecases {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_usecaselist
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vitec_usecaseshow --------------------------------------------------------
|
||||||
|
tt_content.list.20.vitec_usecaseshow = USER
|
||||||
|
tt_content.list.20.vitec_usecaseshow {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.list.fields.content.fields.usecase = USER
|
||||||
|
tt_content.list.fields.content.fields.usecase {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_usecaseshow
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Include container (layout) rendering definitions
|
# Include container (layout) rendering definitions
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
config {
|
||||||
|
pageTitleProviders {
|
||||||
|
vitec {
|
||||||
|
provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider
|
||||||
|
before = record
|
||||||
|
before = seo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Headless: Add products field to vitec_productlist
|
||||||
|
tt_content.list.20.vitec_productlist = USER
|
||||||
|
tt_content.list.20.vitec_productlist {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override the JSON structure for vitec_productlist
|
||||||
|
tt_content.list.fields.content.fields.products = USER
|
||||||
|
tt_content.list.fields.content.fields.products {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
# Headless: Add product field to vitec_productshow
|
||||||
|
tt_content.list.20.vitec_productshow = USER
|
||||||
|
tt_content.list.20.vitec_productshow {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override the JSON structure for vitec_productshow
|
||||||
|
tt_content.list.fields.content.fields.product = USER
|
||||||
|
tt_content.list.fields.content.fields.product {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include container (layout) rendering definitions
|
||||||
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript'
|
||||||
|
|
||||||
|
# Ensure headless's content element JSON definitions take precedence over
|
||||||
|
# fluid_styled_content's HTML definitions. This is loaded at the end of the
|
||||||
|
# Vitecset to guarantee winning load order.
|
||||||
|
@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript'
|
||||||
|
|
||||||
|
# Include menu (navigation) JSON definitions
|
||||||
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript'
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
config {
|
||||||
|
pageTitleProviders {
|
||||||
|
vitec {
|
||||||
|
provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider
|
||||||
|
before = record
|
||||||
|
before = seo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Headless list-plugin JSON renderers
|
||||||
|
#
|
||||||
|
# `tt_content.list.20.<list_type>` renders the element body for that subtype
|
||||||
|
# (only ever invoked for the matching list_type — safe as-is).
|
||||||
|
#
|
||||||
|
# `tt_content.list.fields.content.fields.<key>` is GLOBAL: without scoping it
|
||||||
|
# would inject <key> into EVERY list plugin's content. Each such field is
|
||||||
|
# therefore guarded with:
|
||||||
|
# stdWrap.if { equals.field = list_type ... } -> empty for other plugins
|
||||||
|
# ifEmptyUnsetKey = 1 -> empty key removed entirely
|
||||||
|
# (headless JsonContentObject honours ifEmptyUnsetKey: '' / false => unset).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- vitec_productlist --------------------------------------------------------
|
||||||
|
tt_content.list.20.vitec_productlist = USER
|
||||||
|
tt_content.list.20.vitec_productlist {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.list.fields.content.fields.products = USER
|
||||||
|
tt_content.list.fields.content.fields.products {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_productlist
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vitec_productshow --------------------------------------------------------
|
||||||
|
tt_content.list.20.vitec_productshow = USER
|
||||||
|
tt_content.list.20.vitec_productshow {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.list.fields.content.fields.product = USER
|
||||||
|
tt_content.list.fields.content.fields.product {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_productshow
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vitec_usecaselist --------------------------------------------------------
|
||||||
|
tt_content.list.20.vitec_usecaselist = USER
|
||||||
|
tt_content.list.20.vitec_usecaselist {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.list.fields.content.fields.usecases = USER
|
||||||
|
tt_content.list.fields.content.fields.usecases {
|
||||||
|
userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
||||||
|
stdWrap.if {
|
||||||
|
value = vitec_usecaselist
|
||||||
|
equals.field = list_type
|
||||||
|
}
|
||||||
|
ifEmptyUnsetKey = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include container (layout) rendering definitions
|
||||||
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript'
|
||||||
|
|
||||||
|
# Ensure headless's content element JSON definitions take precedence over
|
||||||
|
# fluid_styled_content's HTML definitions. This is loaded at the end of the
|
||||||
|
# Vitecset to guarantee winning load order.
|
||||||
|
@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript'
|
||||||
|
|
||||||
|
# Include menu (navigation) JSON definitions
|
||||||
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript'
|
||||||
69
packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php
Executable file
69
packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
defined('TYPO3') or die();
|
||||||
|
|
||||||
|
use B13\Container\Tca\ContainerConfiguration;
|
||||||
|
use B13\Container\Tca\Registry;
|
||||||
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
(static function (): void {
|
||||||
|
$l = 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:';
|
||||||
|
|
||||||
|
/** @var Registry $registry */
|
||||||
|
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||||
|
|
||||||
|
$registry->configureContainer(
|
||||||
|
(new ContainerConfiguration(
|
||||||
|
'vitec_container',
|
||||||
|
$l . 'container.title',
|
||||||
|
$l . 'container.description',
|
||||||
|
[
|
||||||
|
[
|
||||||
|
['name' => $l . 'container.column', 'colPos' => 220],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
))
|
||||||
|
->setIcon('EXT:vitec/Resources/Public/Icons/vitec-container.svg')
|
||||||
|
->setGroup('vitec')
|
||||||
|
->setSaveAndCloseInNewContentElementWizard(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
$GLOBALS['TCA']['tt_content']['types']['vitec_container']['showitem'] =
|
||||||
|
'--palette--;;general,
|
||||||
|
header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading,
|
||||||
|
subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline,
|
||||||
|
tx_vitec_bg_variant,
|
||||||
|
pi_flexform;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.label,
|
||||||
|
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
|
||||||
|
--palette--;;frames,
|
||||||
|
--palette--;;appearanceLinks,
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
|
||||||
|
--palette--;;language,
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
|
||||||
|
--palette--;;hidden,
|
||||||
|
--palette--;;access';
|
||||||
|
|
||||||
|
// Bind the Container FlexForm to this CType.
|
||||||
|
// Core tt_content.pi_flexform uses ds_pointerField = 'list_type,CType'.
|
||||||
|
// The official helper registers the data structure under the key
|
||||||
|
// '*,vitec_container' (wildcard list_type + CType), which is the
|
||||||
|
// version-proof way to attach a FlexForm to a CType-based element.
|
||||||
|
ExtensionManagementUtility::addPiFlexFormValue(
|
||||||
|
'*',
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Container.xml',
|
||||||
|
'vitec_container'
|
||||||
|
);
|
||||||
|
|
||||||
|
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
||||||
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
||||||
|
iconIdentifier = vitec-container
|
||||||
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
|
||||||
|
description = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.description
|
||||||
|
tt_content_defValues {
|
||||||
|
CType = vitec_container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TSCONFIG);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
defined('TYPO3') or die();
|
||||||
|
|
||||||
|
use B13\Container\Tca\ContainerConfiguration;
|
||||||
|
use B13\Container\Tca\Registry;
|
||||||
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
(static function (): void {
|
||||||
|
$l = 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:';
|
||||||
|
|
||||||
|
/** @var Registry $registry */
|
||||||
|
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||||
|
|
||||||
|
$registry->configureContainer(
|
||||||
|
(new ContainerConfiguration(
|
||||||
|
'vitec_container',
|
||||||
|
$l . 'container.title',
|
||||||
|
$l . 'container.description',
|
||||||
|
[
|
||||||
|
[
|
||||||
|
['name' => $l . 'container.column', 'colPos' => 220],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
))
|
||||||
|
->setIcon('EXT:vitec/Resources/Public/Icons/vitec-container.svg')
|
||||||
|
->setGroup('vitec')
|
||||||
|
->setSaveAndCloseInNewContentElementWizard(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
$GLOBALS['TCA']['tt_content']['types']['vitec_container']['showitem'] =
|
||||||
|
'--palette--;;general,
|
||||||
|
header;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.heading,
|
||||||
|
subheader;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:section.subline,
|
||||||
|
tx_vitec_bg_variant,
|
||||||
|
pi_flexform,
|
||||||
|
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
|
||||||
|
--palette--;;frames,
|
||||||
|
--palette--;;appearanceLinks,
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
|
||||||
|
--palette--;;language,
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
|
||||||
|
--palette--;;hidden,
|
||||||
|
--palette--;;access';
|
||||||
|
|
||||||
|
// Bind the Container FlexForm to this CType.
|
||||||
|
// Core tt_content.pi_flexform uses ds_pointerField = 'list_type,CType',
|
||||||
|
// so the CType value is a valid data-structure key.
|
||||||
|
$GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds']['vitec_container'] =
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Container.xml';
|
||||||
|
|
||||||
|
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
|
||||||
|
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
|
||||||
|
iconIdentifier = vitec-container
|
||||||
|
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
|
||||||
|
description = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.description
|
||||||
|
tt_content_defValues {
|
||||||
|
CType = vitec_container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TSCONFIG);
|
||||||
|
})();
|
||||||
@@ -87,3 +87,14 @@ tt_content.vitec_cols_33_33_33 =< tt_content.vitec_cols_50_50
|
|||||||
tt_content.vitec_cols_25_25_25_25 =< tt_content.vitec_cols_50_50
|
tt_content.vitec_cols_25_25_25_25 =< tt_content.vitec_cols_50_50
|
||||||
tt_content.vitec_cols_66_33 =< tt_content.vitec_cols_50_50
|
tt_content.vitec_cols_66_33 =< tt_content.vitec_cols_50_50
|
||||||
tt_content.vitec_cols_33_66 =< tt_content.vitec_cols_50_50
|
tt_content.vitec_cols_33_66 =< tt_content.vitec_cols_50_50
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# VITEC · Container — single-column container with a custom CSS class.
|
||||||
|
# Inherits the full structure of the grid containers and adds the `cssClass`
|
||||||
|
# field, read from the element's FlexForm (settings.cssClass).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
tt_content.vitec_container =< tt_content.vitec_cols_50_50
|
||||||
|
tt_content.vitec_container.fields.cssClass = TEXT
|
||||||
|
tt_content.vitec_container.fields.cssClass.data = flexform:pi_flexform:settings.cssClass
|
||||||
|
tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject = TEXT
|
||||||
|
tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject.value =
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# VITEC container content elements — self-contained JSON rendering
|
||||||
|
# Children collected via Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor
|
||||||
|
# (exception-safe; returns [] on any error, never crashes the outer CE).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
tt_content.vitec_cols_50_50 = JSON
|
||||||
|
tt_content.vitec_cols_50_50 {
|
||||||
|
fields {
|
||||||
|
id = INT
|
||||||
|
id.field = uid
|
||||||
|
type = TEXT
|
||||||
|
type.field = CType
|
||||||
|
colPos = INT
|
||||||
|
colPos.field = colPos
|
||||||
|
|
||||||
|
categories = COA
|
||||||
|
categories {
|
||||||
|
10 = CONTENT
|
||||||
|
10 {
|
||||||
|
table = sys_category
|
||||||
|
select {
|
||||||
|
pidInList = root
|
||||||
|
selectFields = sys_category.title
|
||||||
|
join = sys_category_record_mm on sys_category_record_mm.uid_local = sys_category.uid
|
||||||
|
where {
|
||||||
|
field = uid
|
||||||
|
wrap = AND sys_category_record_mm.tablenames = 'tt_content' AND sys_category_record_mm.uid_foreign=|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderObj = TEXT
|
||||||
|
renderObj {
|
||||||
|
field = title
|
||||||
|
wrap = |###BREAK###
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdWrap.split {
|
||||||
|
token = ###BREAK###
|
||||||
|
cObjNum = 1 |*|2|*| 3
|
||||||
|
1 {
|
||||||
|
current = 1
|
||||||
|
stdWrap.wrap = |
|
||||||
|
}
|
||||||
|
2 {
|
||||||
|
current = 1
|
||||||
|
stdWrap.wrap = ,|
|
||||||
|
}
|
||||||
|
3 {
|
||||||
|
current = 1
|
||||||
|
stdWrap.wrap = |
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appearance = JSON
|
||||||
|
appearance {
|
||||||
|
fields {
|
||||||
|
layout = TEXT
|
||||||
|
layout.field = layout
|
||||||
|
frameClass = TEXT
|
||||||
|
frameClass.field = frame_class
|
||||||
|
spaceBefore = TEXT
|
||||||
|
spaceBefore.field = space_before_class
|
||||||
|
spaceAfter = TEXT
|
||||||
|
spaceAfter.field = space_after_class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header = TEXT
|
||||||
|
header.field = header
|
||||||
|
subheader = TEXT
|
||||||
|
subheader.field = subheader
|
||||||
|
tx_vitec_bg_variant = TEXT
|
||||||
|
tx_vitec_bg_variant.field = tx_vitec_bg_variant
|
||||||
|
|
||||||
|
items = JSON
|
||||||
|
items {
|
||||||
|
dataProcessing {
|
||||||
|
10 = Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor
|
||||||
|
10.as = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_content.vitec_cols_33_33_33 =< tt_content.vitec_cols_50_50
|
||||||
|
tt_content.vitec_cols_25_25_25_25 =< tt_content.vitec_cols_50_50
|
||||||
|
tt_content.vitec_cols_66_33 =< tt_content.vitec_cols_50_50
|
||||||
|
tt_content.vitec_cols_33_66 =< tt_content.vitec_cols_50_50
|
||||||
@@ -68,6 +68,28 @@
|
|||||||
<source>Narrow sidebar + wide main column</source>
|
<source>Narrow sidebar + wide main column</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="container.title" resname="container.title">
|
||||||
|
<source>VITEC · Container</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.description" resname="container.description">
|
||||||
|
<source>Generic container for other content elements with a custom CSS class</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.column" resname="container.column">
|
||||||
|
<source>Content</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
|
||||||
|
<source>Container Settings</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.flexform.label" resname="container.flexform.label">
|
||||||
|
<source>Container Settings</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
|
||||||
|
<source>CSS Class</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
|
||||||
|
<source>Custom CSS class that is rendered on the container in the frontend.</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
<trans-unit id="column.1" resname="column.1">
|
<trans-unit id="column.1" resname="column.1">
|
||||||
<source>Column 1</source>
|
<source>Column 1</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||||
|
<xliff version="1.0">
|
||||||
|
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
||||||
|
<header/>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="group.header" resname="group.header">
|
||||||
|
<source>VITEC</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="section.heading" resname="section.heading">
|
||||||
|
<source>Section Heading</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="section.subline" resname="section.subline">
|
||||||
|
<source>Section Subline</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
||||||
|
<source>Background Variant</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
||||||
|
<source>None</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
||||||
|
<source>Orange</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
||||||
|
<source>Blue</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
||||||
|
<source>Graphite</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
||||||
|
<source>Midnight</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
||||||
|
<source>VITEC · Two Columns (50 / 50)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
||||||
|
<source>Two equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
||||||
|
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
||||||
|
<source>Three equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
||||||
|
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
||||||
|
<source>Four equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
||||||
|
<source>VITEC · Two Columns (66 / 33)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
||||||
|
<source>Wide main column + narrow sidebar</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
||||||
|
<source>VITEC · Two Columns (33 / 66)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
||||||
|
<source>Narrow sidebar + wide main column</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="column.1" resname="column.1">
|
||||||
|
<source>Column 1</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.2" resname="column.2">
|
||||||
|
<source>Column 2</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.3" resname="column.3">
|
||||||
|
<source>Column 3</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.4" resname="column.4">
|
||||||
|
<source>Column 4</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.main_66" resname="column.main_66">
|
||||||
|
<source>Main (66%)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
||||||
|
<source>Sidebar (33%)</source>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||||
|
<xliff version="1.0">
|
||||||
|
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
||||||
|
<header/>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="group.header" resname="group.header">
|
||||||
|
<source>VITEC</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="section.heading" resname="section.heading">
|
||||||
|
<source>Section Heading</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="section.subline" resname="section.subline">
|
||||||
|
<source>Section Subline</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
||||||
|
<source>Background Variant</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
||||||
|
<source>None</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
||||||
|
<source>Orange</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
||||||
|
<source>Blue</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
||||||
|
<source>Graphite</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
||||||
|
<source>Midnight</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
||||||
|
<source>VITEC · Two Columns (50 / 50)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
||||||
|
<source>Two equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
||||||
|
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
||||||
|
<source>Three equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
||||||
|
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
||||||
|
<source>Four equal columns</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
||||||
|
<source>VITEC · Two Columns (66 / 33)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
||||||
|
<source>Wide main column + narrow sidebar</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
||||||
|
<source>VITEC · Two Columns (33 / 66)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
||||||
|
<source>Narrow sidebar + wide main column</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="container.title" resname="container.title">
|
||||||
|
<source>VITEC · Container</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.description" resname="container.description">
|
||||||
|
<source>Generic container for other content elements with a custom CSS class</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.column" resname="container.column">
|
||||||
|
<source>Content</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
|
||||||
|
<source>Container Settings</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
|
||||||
|
<source>CSS Class</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
|
||||||
|
<source>Custom CSS class that is rendered on the container in the frontend.</source>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<trans-unit id="column.1" resname="column.1">
|
||||||
|
<source>Column 1</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.2" resname="column.2">
|
||||||
|
<source>Column 2</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.3" resname="column.3">
|
||||||
|
<source>Column 3</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.4" resname="column.4">
|
||||||
|
<source>Column 4</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.main_66" resname="column.main_66">
|
||||||
|
<source>Main (66%)</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
||||||
|
<source>Sidebar (33%)</source>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="mlang_tabs_tab">
|
||||||
|
<source>OG Image</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="mlang_labels_tabdescr">
|
||||||
|
<source>Generate Open Graph images (1200×630) for social media sharing.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="mlang_labels_tablabel">
|
||||||
|
<source>OG Image Generator</source>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||||
xmlns:n="http://typo3.org/ns/GeorgRinger/News/ViewHelpers">
|
xmlns:n="http://typo3.org/ns/GeorgRinger/News/ViewHelpers">
|
||||||
<div class="vitec-administration">
|
<div class="vitec-administration">
|
||||||
<f:flashMessages/>
|
<f:flashMessages queueIdentifier="core.template.flashMessages" />
|
||||||
<h2>Hi</h2>
|
<h2>Hi</h2>
|
||||||
<f:render section="main"/>
|
<f:render section="main"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
288
packages/vitec/Resources/Private/Templates/OgImage/Index.html
Normal file
288
packages/vitec/Resources/Private/Templates/OgImage/Index.html
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||||
|
data-namespace-typo3-fluid="true">
|
||||||
|
|
||||||
|
<f:layout name="Backend/Default" />
|
||||||
|
|
||||||
|
<f:section name="main">
|
||||||
|
|
||||||
|
<div class="module-docheader-bar module-docheader-bar-navigation">
|
||||||
|
<div class="module-docheader-bar-column-left">
|
||||||
|
<h2 class="t3js-title-inlineedit">OG Image Generator</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" id="vitec-ogimage-wrap">
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════
|
||||||
|
LEFT: FORM
|
||||||
|
═══════════════════════════════════════════════════════ -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
|
||||||
|
<f:flashMessages queueIdentifier="core.template.flashMessages" />
|
||||||
|
|
||||||
|
<form action="{f:be.uri(route: 'web_vitecogimage.generate')}" method="post" id="vitecOgForm" data-preview-url="{f:be.uri(route: 'web_vitecogimage.preview')}">
|
||||||
|
|
||||||
|
<!-- Preset quick-picks -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><strong>Quick Presets</strong></div>
|
||||||
|
<div class="card-body d-flex gap-2 flex-wrap">
|
||||||
|
<f:for each="{presets}" as="preset" key="key">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm js-preset"
|
||||||
|
data-preset="{key}"
|
||||||
|
data-bg="{preset.bg_color}"
|
||||||
|
data-title-color="{preset.title_color}"
|
||||||
|
data-subtitle-color="{preset.subtitle_color}">
|
||||||
|
{preset.label}
|
||||||
|
</button>
|
||||||
|
</f:for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><strong>Background</strong></div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input class="form-check-input me-1 js-bg-type" type="radio"
|
||||||
|
name="ogimage[bg_type]" value="color"
|
||||||
|
<f:if condition="{formData.bg_type} == 'color'">checked</f:if>/>
|
||||||
|
Solid colour
|
||||||
|
</label>
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input class="form-check-input me-1 js-bg-type" type="radio"
|
||||||
|
name="ogimage[bg_type]" value="image"
|
||||||
|
<f:if condition="{formData.bg_type} == 'image'">checked</f:if>/>
|
||||||
|
Image
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bg-color-wrap" class="mb-3">
|
||||||
|
<label class="form-label">Background Colour</label>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<input type="color" class="form-control form-control-color js-live"
|
||||||
|
name="ogimage[bg_color]" id="bgColor"
|
||||||
|
value="{formData.bg_color}" />
|
||||||
|
<input type="text" class="form-control js-hex-sync" style="max-width:120px"
|
||||||
|
data-target="bgColor" value="{formData.bg_color}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bg-image-wrap" class="mb-3" style="display:none">
|
||||||
|
<label class="form-label">Background Image</label>
|
||||||
|
<f:if condition="{backgroundImages}">
|
||||||
|
<f:then>
|
||||||
|
<select class="form-select js-live" name="ogimage[bg_image]">
|
||||||
|
<option value="">— none —</option>
|
||||||
|
<f:for each="{backgroundImages}" as="img">
|
||||||
|
<option value="{img.path}"
|
||||||
|
<f:if condition="{formData.bg_image} == {img.path}">selected</f:if>>{img.label}</option>
|
||||||
|
</f:for>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Upload images to <code>fileadmin/og-backgrounds/</code> to list them here.</small>
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<input type="text" class="form-control js-live"
|
||||||
|
name="ogimage[bg_image]"
|
||||||
|
placeholder="/fileadmin/og-backgrounds/my-bg.jpg"
|
||||||
|
value="{formData.bg_image}" />
|
||||||
|
<small class="text-muted">No images found in <code>fileadmin/og-backgrounds/</code>. Enter a path manually, or upload files there.</small>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="form-label">Dark overlay opacity: <span id="overlayLabel">{formData.overlay_opacity}</span>%</label>
|
||||||
|
<input type="range" class="form-range js-live" min="0" max="90" step="5"
|
||||||
|
name="ogimage[overlay_opacity]" value="{formData.overlay_opacity}"
|
||||||
|
oninput="document.getElementById('overlayLabel').textContent=this.value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><strong>Text</strong></div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-8">
|
||||||
|
<label class="form-label">Title</label>
|
||||||
|
<input type="text" class="form-control js-live"
|
||||||
|
name="ogimage[title]" value="{formData.title}"
|
||||||
|
placeholder="Your headline" />
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<input type="number" class="form-control js-live"
|
||||||
|
name="ogimage[title_size]" value="{formData.title_size}"
|
||||||
|
min="20" max="120" />
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Colour</label>
|
||||||
|
<input type="color" class="form-control form-control-color js-live"
|
||||||
|
name="ogimage[title_color]" id="titleColor"
|
||||||
|
value="{formData.title_color}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-8">
|
||||||
|
<label class="form-label">Subtitle / Description</label>
|
||||||
|
<input type="text" class="form-control js-live"
|
||||||
|
name="ogimage[subtitle]" value="{formData.subtitle}"
|
||||||
|
placeholder="Short description" />
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<input type="number" class="form-control js-live"
|
||||||
|
name="ogimage[subtitle_size]" value="{formData.subtitle_size}"
|
||||||
|
min="14" max="80" />
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Colour</label>
|
||||||
|
<input type="color" class="form-control form-control-color js-live"
|
||||||
|
name="ogimage[subtitle_color]" id="subtitleColor"
|
||||||
|
value="{formData.subtitle_color}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label / Badge -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><strong>Label / Badge</strong></div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Label text (leave blank to hide)</label>
|
||||||
|
<input type="text" class="form-control js-live"
|
||||||
|
name="ogimage[label_text]" value="{formData.label_text}"
|
||||||
|
placeholder="e.g. VITEC" />
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<label class="form-label">Background</label>
|
||||||
|
<input type="color" class="form-control form-control-color js-live"
|
||||||
|
name="ogimage[label_bg_color]" id="labelBg"
|
||||||
|
value="{formData.label_bg_color}" />
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<label class="form-label">Text colour</label>
|
||||||
|
<input type="color" class="form-control form-control-color js-live"
|
||||||
|
name="ogimage[label_text_color]" id="labelText"
|
||||||
|
value="{formData.label_text_color}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Position</label>
|
||||||
|
<select class="form-select js-live" name="ogimage[label_position]">
|
||||||
|
<option value="top-left" <f:if condition="{formData.label_position} == 'top-left'">selected</f:if>>Top Left</option>
|
||||||
|
<option value="top-right" <f:if condition="{formData.label_position} == 'top-right'">selected</f:if>>Top Right</option>
|
||||||
|
<option value="bottom-left" <f:if condition="{formData.label_position} == 'bottom-left'">selected</f:if>>Bottom Left</option>
|
||||||
|
<option value="bottom-right"<f:if condition="{formData.label_position} == 'bottom-right'">selected</f:if>>Bottom Right</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><strong>Output</strong></div>
|
||||||
|
<div class="card-body row">
|
||||||
|
<div class="col-4">
|
||||||
|
<label class="form-label">Format</label>
|
||||||
|
<select class="form-select" name="ogimage[output_format]">
|
||||||
|
<option value="jpg" <f:if condition="{formData.output_format} == 'jpg'">selected</f:if>>JPG</option>
|
||||||
|
<option value="png" <f:if condition="{formData.output_format} == 'png'">selected</f:if>>PNG</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<label class="form-label">Quality (JPG): <span id="qualLabel">{formData.output_quality}</span></label>
|
||||||
|
<input type="range" class="form-range" min="50" max="100" step="5"
|
||||||
|
name="ogimage[output_quality]" value="{formData.output_quality}"
|
||||||
|
oninput="document.getElementById('qualLabel').textContent=this.value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mb-4">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btnPreview">
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Save Image to fileadmin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div><!-- /col form -->
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════
|
||||||
|
RIGHT: PREVIEW + SAVED IMAGES
|
||||||
|
═══════════════════════════════════════════════════════ -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<strong>Preview <small class="text-muted">(1200 × 630)</small></strong>
|
||||||
|
<span id="previewSpinner" class="spinner-border spinner-border-sm text-secondary d-none" role="status"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 bg-secondary">
|
||||||
|
<div style="position:relative; padding-top: 52.5%;"><!-- 630/1200 -->
|
||||||
|
<img id="ogPreviewImg" src="" alt="preview"
|
||||||
|
style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:none;" />
|
||||||
|
<div id="ogPreviewPlaceholder"
|
||||||
|
style="position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;">
|
||||||
|
Click "Preview" to render
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-end">
|
||||||
|
<a id="previewDownload" href="#" download="og-preview.png"
|
||||||
|
class="btn btn-sm btn-outline-secondary d-none">
|
||||||
|
Download preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<f:if condition="{savedImages}">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><strong>Recent saved images</strong></div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead><tr><th>File</th><th>Created</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<f:for each="{savedImages}" as="img">
|
||||||
|
<tr>
|
||||||
|
<td class="text-truncate" style="max-width:140px">{img.name}</td>
|
||||||
|
<td><small>{img.created}</small></td>
|
||||||
|
<td>
|
||||||
|
<a href="{img.path}" target="_blank" class="btn btn-xs btn-outline-primary p-1 lh-1">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</f:for>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
</div><!-- /col preview -->
|
||||||
|
|
||||||
|
</div><!-- /row -->
|
||||||
|
|
||||||
|
</f:section>
|
||||||
|
|
||||||
|
</html>
|
||||||
6
packages/vitec/Resources/Public/Icons/vitec-container.svg
Executable file
6
packages/vitec/Resources/Public/Icons/vitec-container.svg
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||||
|
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
|
||||||
|
<rect x="3.5" y="4.5" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.55"/>
|
||||||
|
<rect x="3.5" y="7.6" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.35"/>
|
||||||
|
<rect x="3.5" y="10.7" width="9" height="1.6" rx="0.3" fill="#000" opacity="0.2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 447 B |
7
packages/vitec/Resources/Public/Icons/vitec-ogimage.svg
Normal file
7
packages/vitec/Resources/Public/Icons/vitec-ogimage.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||||
|
<rect x="1" y="3" width="14" height="10" rx="1.5" fill="#e63946"/>
|
||||||
|
<rect x="1" y="3" width="14" height="10" rx="1.5" stroke="#c1121f" stroke-width="0.5"/>
|
||||||
|
<text x="2" y="10.5" font-size="4.5" font-family="sans-serif" fill="#fff" font-weight="bold">OG</text>
|
||||||
|
<circle cx="11.5" cy="6.5" r="2" fill="#fff" opacity="0.9"/>
|
||||||
|
<polyline points="1,11 5,7 8,9.5 11,6 15,10" stroke="#fff" stroke-width="1" fill="none" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 523 B |
173
packages/vitec/Resources/Public/Javascript/og-image-preview.js
Normal file
173
packages/vitec/Resources/Public/Javascript/og-image-preview.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* OG Image Backend Module – live preview via AJAX
|
||||||
|
* EXT:vitec/Resources/Public/Javascript/og-image-preview.js
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** Debounce helper */
|
||||||
|
function debounce(fn, ms) {
|
||||||
|
let t;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(() => fn.apply(this, args), ms);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collect all form values into a FormData-compatible plain object */
|
||||||
|
function collectFormData() {
|
||||||
|
const form = document.getElementById('vitecOgForm');
|
||||||
|
if (!form) return {};
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const out = {};
|
||||||
|
for (const [key, val] of fd.entries()) {
|
||||||
|
// key looks like "ogimage[something]"
|
||||||
|
const match = key.match(/^ogimage\[(.+)]$/);
|
||||||
|
if (match) {
|
||||||
|
out[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST to the preview endpoint, update the <img> */
|
||||||
|
async function requestPreview() {
|
||||||
|
const form = document.getElementById('vitecOgForm');
|
||||||
|
const spinner = document.getElementById('previewSpinner');
|
||||||
|
const img = document.getElementById('ogPreviewImg');
|
||||||
|
const placeholder = document.getElementById('ogPreviewPlaceholder');
|
||||||
|
const dl = document.getElementById('previewDownload');
|
||||||
|
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
if (spinner) spinner.classList.remove('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = new URLSearchParams(collectFormData());
|
||||||
|
const previewUrl = form.dataset.previewUrl || window.location.href;
|
||||||
|
|
||||||
|
const resp = await fetch(previewUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: body.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) throw new Error('Server error ' + resp.status);
|
||||||
|
|
||||||
|
const json = await resp.json();
|
||||||
|
|
||||||
|
if (json.success && json.dataUri) {
|
||||||
|
img.src = json.dataUri;
|
||||||
|
img.style.display = 'block';
|
||||||
|
if (placeholder) placeholder.style.display = 'none';
|
||||||
|
|
||||||
|
if (dl) {
|
||||||
|
dl.href = json.dataUri;
|
||||||
|
dl.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[OG Preview]', json.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OG Preview] fetch failed:', e);
|
||||||
|
} finally {
|
||||||
|
if (spinner) spinner.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedPreview = debounce(requestPreview, 600);
|
||||||
|
|
||||||
|
// ── Wire up events once DOM is ready ──────────────────────────────
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
|
// Manual preview button
|
||||||
|
const btn = document.getElementById('btnPreview');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', requestPreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-preview on any .js-live input change
|
||||||
|
document.querySelectorAll('.js-live').forEach(function (el) {
|
||||||
|
el.addEventListener('input', debouncedPreview);
|
||||||
|
el.addEventListener('change', debouncedPreview);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Background type toggle (show colour vs image picker)
|
||||||
|
function syncBgType() {
|
||||||
|
const colorWrap = document.getElementById('bg-color-wrap');
|
||||||
|
const imageWrap = document.getElementById('bg-image-wrap');
|
||||||
|
const selected = document.querySelector('input.js-bg-type:checked');
|
||||||
|
if (!selected) return;
|
||||||
|
if (selected.value === 'image') {
|
||||||
|
colorWrap && (colorWrap.style.display = 'none');
|
||||||
|
imageWrap && (imageWrap.style.display = 'block');
|
||||||
|
} else {
|
||||||
|
colorWrap && (colorWrap.style.display = 'block');
|
||||||
|
imageWrap && (imageWrap.style.display = 'none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('input.js-bg-type').forEach(function (el) {
|
||||||
|
el.addEventListener('change', function () {
|
||||||
|
syncBgType();
|
||||||
|
debouncedPreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bgImageSelect = document.querySelector('select[name="ogimage[bg_image]"]');
|
||||||
|
if (bgImageSelect) {
|
||||||
|
bgImageSelect.addEventListener('change', function () {
|
||||||
|
const imageTypeRadio = document.querySelector('input.js-bg-type[value="image"]');
|
||||||
|
if (imageTypeRadio) {
|
||||||
|
imageTypeRadio.checked = true;
|
||||||
|
}
|
||||||
|
syncBgType();
|
||||||
|
requestPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
syncBgType(); // initialise on load
|
||||||
|
|
||||||
|
// Hex text ↔ colour picker sync
|
||||||
|
document.querySelectorAll('.js-hex-sync').forEach(function (hexInput) {
|
||||||
|
const targetId = hexInput.dataset.target;
|
||||||
|
const picker = document.getElementById(targetId);
|
||||||
|
if (!picker) return;
|
||||||
|
|
||||||
|
picker.addEventListener('input', function () {
|
||||||
|
hexInput.value = picker.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
hexInput.addEventListener('input', function () {
|
||||||
|
const val = hexInput.value.trim();
|
||||||
|
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
|
||||||
|
picker.value = val;
|
||||||
|
picker.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preset buttons
|
||||||
|
document.querySelectorAll('.js-preset').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const bg = btn.dataset.bg;
|
||||||
|
const tc = btn.dataset.titleColor;
|
||||||
|
const sc = btn.dataset.subtitleColor;
|
||||||
|
|
||||||
|
const bgInput = document.getElementById('bgColor');
|
||||||
|
if (bgInput && bg) { bgInput.value = bg; bgInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||||
|
|
||||||
|
const tInput = document.getElementById('titleColor');
|
||||||
|
if (tInput && tc) { tInput.value = tc; tInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||||
|
|
||||||
|
const sInput = document.getElementById('subtitleColor');
|
||||||
|
if (sInput && sc) { sInput.value = sc; sInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||||
|
|
||||||
|
debouncedPreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -4,6 +4,14 @@ defined('TYPO3') || die();
|
|||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||||
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||||
|
|
||||||
|
// Configure view template paths for the OG Image backend module
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['vitec']['view'] = [
|
||||||
|
'templateRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Templates/'],
|
||||||
|
'layoutRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Layouts/'],
|
||||||
|
'partialRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Partials/'],
|
||||||
|
];
|
||||||
|
|
||||||
// Register VITEC Backend Layout data provider — 21 BE layouts, one per pages.layout (0..20)
|
// Register VITEC Backend Layout data provider — 21 BE layouts, one per pages.layout (0..20)
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['vitec']
|
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['vitec']
|
||||||
= \Evomedien\Vitec\View\BackendLayoutDataProvider::class;
|
= \Evomedien\Vitec\View\BackendLayoutDataProvider::class;
|
||||||
|
|||||||
Reference in New Issue
Block a user