VITEC headless v14: 5-bug fix unifies plugin renderers + cleanup

Resolves a cascade of TYPO3 v14 breaking changes that left every VITEC
plugin's JSON output silently empty:

1. `list_type` column dropped — all renderer page-discovery queries now
   filter by `CType = 'vitec_X'`.
2. `#[AsAllowedCallable]` attribute added to every public render() —
   without it v14 throws AllowedCallableException, which headless
   silently filters via its "Oops, an error occurred!" guard.
3. `$GLOBALS['TSFE']->id` is null inside JSON cObj context — page id
   now read from the `frontend.page.information` request attribute
   (TSFE fallback kept for legacy entry points).
4. page.tsconfig wizard items migrated to `CType = vitec_X` directly
   (legacy `CType=list, list_type=...` no longer exists in v14).
5. ContainerChildrenProcessor + ContentElementResolver dispatch the
   PLUGIN_RENDERERS map by CType (primary) with list_type fallback.

Beyond the bug fix this commit also contains:
- Datasheets renderer rewritten to "products with newest datasheet"
  semantics (filtered via Download.hideondatasheets).
- New vitec/card content block — multi-purpose card with backend
  preview and layout/background/aspect/alignment/border/shadow options.
- New vitec_container CType (b13 single-column container, FlexForm
  cssClass propagated into the JSON envelope).
- ContentElementResolver service for contentelement/contentelementcta
  link resolution; ContainerChildrenProcessor for nested plugin
  resolution inside b13 containers.
- Vitecset setup.typoscript: ContentElement import moved to top so
  lib.contentElement is defined before VITEC's tt_content blocks.
- Cleanup: 98 .bak.<timestamp> debugging backups removed; *.bak.* and
  /public/_assets_install/ added to .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
o-rasche
2026-06-15 14:29:17 +02:00
parent 64ea55d29d
commit 3aaf8fdad5
98 changed files with 5650 additions and 10154 deletions

View File

@@ -3,6 +3,13 @@ declare(strict_types=1);
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 Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -15,6 +22,11 @@ use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
* { id, type, colPos, sorting, appearance, data }
* `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.
*/
final class ContainerChildrenProcessor implements DataProcessorInterface
@@ -27,7 +39,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
/** 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',
@@ -38,8 +49,6 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
'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',
@@ -53,6 +62,22 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
'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'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
];
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
@@ -124,6 +149,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
$data[$field] = $this->castValue($field, $value);
}
// Resolve nested VITEC list-plugins to their headless JSON.
$this->resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
@@ -139,6 +167,52 @@ 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
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
// 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
{
return $value === null

View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
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 Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer;
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.
*
* 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.
*/
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 = [
'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
'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',
];
/**
* 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'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
];
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);
}
// Resolve nested VITEC list-plugins to their headless JSON.
$this->resolvePluginData($record, $data);
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,
];
}
/**
* 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
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
// 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
{
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;
}
}

View File

@@ -7,6 +7,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer;
use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -70,6 +75,11 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'],
];
public function process(
@@ -171,13 +181,17 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
*/
private function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) {
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {

0
packages/vitec/Classes/Domain/Model/Product.php Executable file → Normal file
View File

View File

@@ -1,922 +0,0 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Domain\Model;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Domain\Model\Category;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
/**
* This file is part of the "VITEC" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2025
*/
/**
* Product
*/
class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* title
*
* @var string
* @TYPO3\CMS\Extbase\Annotation\Validate("NotEmpty")
*/
protected $title;
/**
* slug
*
* @var string
*/
protected $slug;
/**
* urltitle
*
* @var string
*/
protected $urltitle = '';
/**
* seotitle
*
* @var string
*/
protected $seotitle = '';
/**
* seometa
*
* @var string
*/
protected $seometa = '';
/**
* keywords
*
* @var string
*/
protected $keywords = '';
/**
* structureddata
*
* @var string
*/
protected $structureddata = '';
/**
* teaser
*
* @var string
*/
protected $teaser = '';
/**
* subtitle
*
* @var string
*/
protected $subtitle = '';
/**
* video
*
* @var string
*/
protected $video = '';
/**
* hideonapp
*
* @var bool
*/
protected $hideonapp = false;
/**
* hideonwebsite
*
* @var bool
*/
protected $hideonwebsite = false;
/**
* hideondatasheets
*
* @var bool
*/
protected $hideondatasheets = false;
/**
* hideonproducts
*
* @var bool
*/
protected $hideonproducts = false;
/**
* applications
*
* @var string
*/
protected $applications = '';
/**
* description
*
* @var string
*/
protected $description = '';
/**
* highlights
*
* @var string
*/
protected $highlights = '';
/**
* shortcut
*
* @var bool
*/
protected $shortcut = false;
/**
* shortcutpid
*
* @var string
*/
protected $shortcutpid = '';
/**
* legacy
*
* @var bool
*/
protected $legacy = false;
/**
* supportproduct
*
* @var bool
*/
protected $supportproduct = false;
/**
* subproduct
*
* @var bool
*/
protected $subproduct = false;
/**
* @var ObjectStorage<Category>
*/
protected $categories;
/**
* Product images
*
* @var ObjectStorage<FileReference>
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $productimage;
/**
* Downloads
*
* @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $downloads;
/**
* Open Graph Image
*
* @var FileReference
*/
protected $ogimage;
/**
* Content element link - Key Features
*
* @var string
*/
protected $contentelement;
/**
* Content element link - CTA Features
*
* @var string
*/
protected $contentelementcta;
/**
* @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Evomedien\Vitec\Domain\Model\Product>
* @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $relatedprodukt;
/* --------------------------------------------------------------------- */
/**
* Returns the title
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Sets the title
*
* @param string $title
* @return void
*/
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* Returns the video
*
* @return string
*/
public function getVideo()
{
return $this->video;
}
/**
* Sets the video
*
* @param string $video
* @return void
*/
public function setVideo(string $video)
{
$this->video = $video;
}
/**
* Returns the slug
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* Sets the slug
*
* @param string $slug
* @return void
*/
public function setSlug(string $slug)
{
$this->slug = $slug;
}
/**
* Returns the urltitle
*
* @return string
*/
public function getUrltitle()
{
return $this->urltitle;
}
/**
* Sets the urltitle
*
* @param string $urltitle
* @return void
*/
public function setUrltitle(string $urltitle)
{
$this->urltitle = $urltitle;
}
/**
* Returns the seotitle
*
* @return string
*/
public function getSeotitle()
{
return $this->seotitle;
}
/**
* Sets the seotitle
*
* @param string $seotitle
* @return void
*/
public function setSeotitle(string $seotitle)
{
$this->seotitle = $seotitle;
}
/**
* Returns the seometa
*
* @return string $seometa
*/
public function getSeometa()
{
return $this->seometa;
}
/**
* Sets the seometa
*
* @param string $seometa
* @return void
*/
public function setSeometa($seometa)
{
$this->seometa = $seometa;
}
/**
* Returns the keywords
*
* @return string $keywords
*/
public function getKeywords()
{
return $this->keywords;
}
/**
* Sets the keywords
*
* @param string $keywords
* @return void
*/
public function setKeywords($keywords)
{
$this->keywords = $keywords;
}
/**
* Returns the teaser
*
* @return string $kteasereywords
*/
public function getTeaser()
{
return $this->teaser;
}
/**
* Sets the teaser
*
* @param string $teaser
* @return void
*/
public function setTeaser($teaser)
{
$this->teaser = $teaser;
}
/**
* Returns the subtitle
*
* @return string $subtitle
*/
public function getSubtitle()
{
return $this->subtitle;
}
/**
* Sets the subtitle
*
* @param string $subtitle
* @return void
*/
public function setSubtitle($subtitle)
{
$this->subtitle = $subtitle;
}
/**
* Returns the hideonapp
*
* @return bool $hideonapp
*/
public function getHideonapp()
{
return $this->hideonapp;
}
/**
* Sets the hideonapp
*
* @param bool $hideonapp
* @return void
*/
public function setHideonapp($hideonapp)
{
$this->hideonapp = $hideonapp;
}
/**
* Returns the description
*
* @return bool $description
*/
public function getDescription()
{
return $this->description;
}
/**
* Sets the description
*
* @param bool $description
* @return void
*/
public function setDescriptionp($description)
{
$this->description = $description;
}
/**
* Returns the hideonwebsite
*
* @return bool $hideonwebsite
*/
public function getHideonwebsite()
{
return $this->hideonwebsite;
}
/**
* Sets the hideonwebsite
*
* @param bool $hideonwebsite
* @return void
*/
public function setHideonwebsite($hideonwebsite)
{
$this->hideonwebsite = $hideonwebsite;
}
/**
* Returns the hideondatasheets
*
* @return bool $hideondatasheets
*/
public function getHideondatasheets()
{
return $this->hideondatasheets;
}
/**
* Sets the hideondatasheets
*
* @param bool $hideondatasheets
* @return void
*/
public function setHideondatasheets($hideondatasheets)
{
$this->hideondatasheets = $hideondatasheets;
}
/**
* Returns the hideonproducts
*
* @return bool $hideonproducts
*/
public function getHideonproducts()
{
return $this->hideonproducts;
}
/**
* Sets the hideonproducts
*
* @param bool $hideonproducts
* @return void
*/
public function setHideonproducts($hideonproducts)
{
$this->hideonproducts = $hideonproducts;
}
/**
* Returns the structureddata
*
* @return string $structureddata
*/
public function getStructureddata()
{
return $this->structureddata;
}
/**
* Sets the structureddata
*
* @param string $structureddata
* @return void
*/
public function setStructureddata($structureddata)
{
$this->structureddata = $structureddata;
}
/**
* Returns the applications
*
* @return string $applications
*/
public function getApplications()
{
return $this->applications;
}
/**
* Sets the applications
*
* @param string $applications
* @return void
*/
public function setApplications($applications)
{
$this->applications = $applications;
}
/**
* Returns the highlights
*
* @return string $highlights
*/
public function getHighlights()
{
return $this->highlights;
}
/**
* Sets the highlights
*
* @param string $highlights
* @return void
*/
public function setHighlights($highlights)
{
$this->highlights = $highlights;
}
/**
* Returns the shortcutpid
*
* @return string
*/
public function getShortcutpid()
{
return $this->shortcutpid;
}
/**
* Sets the shortcutpid
*
* @param string $shortcutpid
* @return void
*/
public function setShortcutpid ($shortcutpid)
{
$this->shortcutpid = $shortcutpid;
}
/**
* Returns the shortcut
*
* @return bool $shortcut
*/
public function getShortcut()
{
return $this->shortcut;
}
/**
* Sets the shortcut
*
* @param bool $shortcut
* @return void
*/
public function setShortcut($shortcut)
{
$this->shortcut = $shortcut;
}
/**
* Returns the legacy
*
* @return bool $legacy
*/
public function getLegacy()
{
return $this->legacy;
}
/**
* Sets the legacy
*
* @param bool $legacy
* @return void
*/
public function setLegacy($legacy)
{
$this->legacy = $legacy;
}
/**
* Returns the supportproduct
*
* @return bool $supportproduct
*/
public function getSupportproduct()
{
return $this->supportproduct;
}
/**
* Sets the supportproduct
*
* @param bool $supportproduct
* @return void
*/
public function setSupportproduct($supportproduct)
{
$this->supportproduct = $supportproduct;
}
/**
* Returns the subproduct
*
* @return bool $subproduct
*/
public function getSubproduct()
{
return $this->subproduct;
}
/**
* Sets the subproduct
*
* @param bool $subproduct
* @return void
*/
public function setSubproduct($subproduct)
{
$this->subproduct = $subproduct;
}
/**
* Initializes the ObjectStorage for categories
*/
public function __construct()
{
$this->categories = new ObjectStorage();
$this->productimage = new ObjectStorage();
$this->downloads = new ObjectStorage();
}
/**
* Adds a category
*
* @param Category $category
* @return void
*/
public function addCategory(Category $category)
{
$this->categories->attach($category);
}
/**
* Removes a category
*
* @param Category $categoryToRemove
* @return void
*/
public function removeCategory(Category $categoryToRemove)
{
$this->categories->detach($categoryToRemove);
}
/**
* Returns the categories
*
* @return ObjectStorage<Category>
*/
public function getCategories()
{
return $this->categories;
}
/**
* Sets the categories
*
* @param ObjectStorage<Category> $categories
* @return void
*/
public function setCategories(ObjectStorage $categories)
{
$this->categories = $categories;
}
/**
* Adds a product image
*
* @param FileReference $productimage
* @return void
*/
public function addProductimage(FileReference $productimage)
{
$this->productimage->attach($productimage);
}
/**
* Removes a product image
*
* @param FileReference $productimageToRemove
* @return void
*/
public function removeProductimage(FileReference $productimageToRemove)
{
$this->productimage->detach($productimageToRemove);
}
/**
* Returns the product images
*
* @return ObjectStorage<FileReference>
*/
public function getProductimage()
{
return $this->productimage;
}
/**
* Sets the product images
*
* @param ObjectStorage<FileReference> $productimage
* @return void
*/
public function setProductimage(ObjectStorage $productimage)
{
$this->productimage = $productimage;
}
/**
* Adds a download
*
* @param \Evomedien\Vitec\Domain\Model\Download $download
* @return void
*/
public function addDownload(\Evomedien\Vitec\Domain\Model\Download $download): void
{
$this->downloads->attach($download);
}
/**
* Removes a download
*
* @param \Evomedien\Vitec\Domain\Model\Download $downloadToRemove
* @return void
*/
public function removeDownload(\Evomedien\Vitec\Domain\Model\Download $downloadToRemove): void
{
$this->downloads->detach($downloadToRemove);
}
/**
* Returns the downloads
*
* @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
*/
public function getDownloads(): ObjectStorage
{
return $this->downloads;
}
/**
* Sets the downloads
*
* @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $downloads
* @return void
*/
public function setDownloads(ObjectStorage $downloads): void
{
$this->downloads = $downloads;
}
/**
* Returns the Open Graph Image
*
* @return FileReference|null
*/
public function getOgimage(): ?FileReference
{
return $this->ogimage;
}
/**
* Sets the Open Graph Image
*
* @param FileReference $ogimage
* @return void
*/
public function setOgimage(FileReference $ogimage): void
{
$this->ogimage = $ogimage;
}
/**
* Returns the content element link
*
* @return string
*/
public function getContentelement(): string
{
return $this->contentelement;
}
/**
* Sets the content element link
*
* @param string $contentelement
* @return void
*/
public function setContentelement(string $contentelement): void
{
$this->contentelement = $contentelement;
}
/**
* Returns the content element cta link
*
* @return string
*/
public function getContentelementcta(): string
{
return $this->contentelementcta;
}
/**
* Sets the content element link cta
*
* @param string $contentelementcta
* @return void
*/
public function setContentelementcta(string $contentelementcta): void
{
$this->contentelementcta = $contentelementcta;
}
/**
* @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product>
*/
public function getRelatedprodukt(): \TYPO3\CMS\Extbase\Persistence\ObjectStorage
{
return $this->relatedprodukt;
}
/**
* @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> $relatedprodukt
*/
public function setRelatedprodukt(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $relatedprodukt): void
{
$this->relatedprodukt = $relatedprodukt;
}
/* --------------------------------------------------------------------- */
}

View File

@@ -1,952 +0,0 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Domain\Model;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Domain\Model\Category;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
/**
* This file is part of the "VITEC" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2025
*/
/**
* Product
*/
class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* title
*
* @var string
* @TYPO3\CMS\Extbase\Annotation\Validate("NotEmpty")
*/
protected $title;
/**
* slug
*
* @var string
*/
protected $slug;
/**
* urltitle
*
* @var string
*/
protected $urltitle = '';
/**
* seotitle
*
* @var string
*/
protected $seotitle = '';
/**
* seometa
*
* @var string
*/
protected $seometa = '';
/**
* keywords
*
* @var string
*/
protected $keywords = '';
/**
* structureddata
*
* @var string
*/
protected $structureddata = '';
/**
* teaser
*
* @var string
*/
protected $teaser = '';
/**
* subtitle
*
* @var string
*/
protected $subtitle = '';
/**
* video
*
* @var string
*/
protected $video = '';
/**
* hideonapp
*
* @var bool
*/
protected $hideonapp = false;
/**
* hideonwebsite
*
* @var bool
*/
protected $hideonwebsite = false;
/**
* hideondatasheets
*
* @var bool
*/
protected $hideondatasheets = false;
/**
* hideonproducts
*
* @var bool
*/
protected $hideonproducts = false;
/**
* applications
*
* @var string
*/
protected $applications = '';
/**
* description
*
* @var string
*/
protected $description = '';
/**
* highlights
*
* @var string
*/
protected $highlights = '';
/**
* shortcut
*
* @var bool
*/
protected $shortcut = false;
/**
* shortcutpid
*
* @var string
*/
protected $shortcutpid = '';
/**
* legacy
*
* @var bool
*/
protected $legacy = false;
/**
* supportproduct
*
* @var bool
*/
protected $supportproduct = false;
/**
* subproduct
*
* @var bool
*/
protected $subproduct = false;
/**
* @var ObjectStorage<Category>
*/
protected $categories;
/**
* Product images
*
* @var ObjectStorage<FileReference>
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $productimage;
/**
* Downloads
*
* @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $downloads;
/**
* Open Graph Image
*
* @var FileReference
*/
protected $ogimage;
/**
* Content element link - Key Features
*
* @var string
*/
/**
* Uploaded / locally selected video file (FAL)
*
* @var \TYPO3\CMS\Extbase\Domain\Model\FileReference|null
*/
protected $videofile;
protected $contentelement;
/**
* Content element link - CTA Features
*
* @var string
*/
protected $contentelementcta;
/**
* @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Evomedien\Vitec\Domain\Model\Product>
* @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
* @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $relatedprodukt;
/* --------------------------------------------------------------------- */
/**
* Returns the title
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Sets the title
*
* @param string $title
* @return void
*/
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* Returns the video
*
* @return string
*/
public function getVideo()
{
return $this->video;
}
/**
* Sets the video
*
* @param string $video
* @return void
*/
public function setVideo(string $video)
{
$this->video = $video;
}
/**
* Returns the slug
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* Sets the slug
*
* @param string $slug
* @return void
*/
public function setSlug(string $slug)
{
$this->slug = $slug;
}
/**
* Returns the urltitle
*
* @return string
*/
public function getUrltitle()
{
return $this->urltitle;
}
/**
* Sets the urltitle
*
* @param string $urltitle
* @return void
*/
public function setUrltitle(string $urltitle)
{
$this->urltitle = $urltitle;
}
/**
* Returns the seotitle
*
* @return string
*/
public function getSeotitle()
{
return $this->seotitle;
}
/**
* Sets the seotitle
*
* @param string $seotitle
* @return void
*/
public function setSeotitle(string $seotitle)
{
$this->seotitle = $seotitle;
}
/**
* Returns the seometa
*
* @return string $seometa
*/
public function getSeometa()
{
return $this->seometa;
}
/**
* Sets the seometa
*
* @param string $seometa
* @return void
*/
public function setSeometa($seometa)
{
$this->seometa = $seometa;
}
/**
* Returns the keywords
*
* @return string $keywords
*/
public function getKeywords()
{
return $this->keywords;
}
/**
* Sets the keywords
*
* @param string $keywords
* @return void
*/
public function setKeywords($keywords)
{
$this->keywords = $keywords;
}
/**
* Returns the teaser
*
* @return string $kteasereywords
*/
public function getTeaser()
{
return $this->teaser;
}
/**
* Sets the teaser
*
* @param string $teaser
* @return void
*/
public function setTeaser($teaser)
{
$this->teaser = $teaser;
}
/**
* Returns the subtitle
*
* @return string $subtitle
*/
public function getSubtitle()
{
return $this->subtitle;
}
/**
* Sets the subtitle
*
* @param string $subtitle
* @return void
*/
public function setSubtitle($subtitle)
{
$this->subtitle = $subtitle;
}
/**
* Returns the hideonapp
*
* @return bool $hideonapp
*/
public function getHideonapp()
{
return $this->hideonapp;
}
/**
* Sets the hideonapp
*
* @param bool $hideonapp
* @return void
*/
public function setHideonapp($hideonapp)
{
$this->hideonapp = $hideonapp;
}
/**
* Returns the description
*
* @return bool $description
*/
public function getDescription()
{
return $this->description;
}
/**
* Sets the description
*
* @param bool $description
* @return void
*/
public function setDescriptionp($description)
{
$this->description = $description;
}
/**
* Returns the hideonwebsite
*
* @return bool $hideonwebsite
*/
public function getHideonwebsite()
{
return $this->hideonwebsite;
}
/**
* Sets the hideonwebsite
*
* @param bool $hideonwebsite
* @return void
*/
public function setHideonwebsite($hideonwebsite)
{
$this->hideonwebsite = $hideonwebsite;
}
/**
* Returns the hideondatasheets
*
* @return bool $hideondatasheets
*/
public function getHideondatasheets()
{
return $this->hideondatasheets;
}
/**
* Sets the hideondatasheets
*
* @param bool $hideondatasheets
* @return void
*/
public function setHideondatasheets($hideondatasheets)
{
$this->hideondatasheets = $hideondatasheets;
}
/**
* Returns the hideonproducts
*
* @return bool $hideonproducts
*/
public function getHideonproducts()
{
return $this->hideonproducts;
}
/**
* Sets the hideonproducts
*
* @param bool $hideonproducts
* @return void
*/
public function setHideonproducts($hideonproducts)
{
$this->hideonproducts = $hideonproducts;
}
/**
* Returns the structureddata
*
* @return string $structureddata
*/
public function getStructureddata()
{
return $this->structureddata;
}
/**
* Sets the structureddata
*
* @param string $structureddata
* @return void
*/
public function setStructureddata($structureddata)
{
$this->structureddata = $structureddata;
}
/**
* Returns the applications
*
* @return string $applications
*/
public function getApplications()
{
return $this->applications;
}
/**
* Sets the applications
*
* @param string $applications
* @return void
*/
public function setApplications($applications)
{
$this->applications = $applications;
}
/**
* Returns the highlights
*
* @return string $highlights
*/
public function getHighlights()
{
return $this->highlights;
}
/**
* Sets the highlights
*
* @param string $highlights
* @return void
*/
public function setHighlights($highlights)
{
$this->highlights = $highlights;
}
/**
* Returns the shortcutpid
*
* @return string
*/
public function getShortcutpid()
{
return $this->shortcutpid;
}
/**
* Sets the shortcutpid
*
* @param string $shortcutpid
* @return void
*/
public function setShortcutpid ($shortcutpid)
{
$this->shortcutpid = $shortcutpid;
}
/**
* Returns the shortcut
*
* @return bool $shortcut
*/
public function getShortcut()
{
return $this->shortcut;
}
/**
* Sets the shortcut
*
* @param bool $shortcut
* @return void
*/
public function setShortcut($shortcut)
{
$this->shortcut = $shortcut;
}
/**
* Returns the legacy
*
* @return bool $legacy
*/
public function getLegacy()
{
return $this->legacy;
}
/**
* Sets the legacy
*
* @param bool $legacy
* @return void
*/
public function setLegacy($legacy)
{
$this->legacy = $legacy;
}
/**
* Returns the supportproduct
*
* @return bool $supportproduct
*/
public function getSupportproduct()
{
return $this->supportproduct;
}
/**
* Sets the supportproduct
*
* @param bool $supportproduct
* @return void
*/
public function setSupportproduct($supportproduct)
{
$this->supportproduct = $supportproduct;
}
/**
* Returns the subproduct
*
* @return bool $subproduct
*/
public function getSubproduct()
{
return $this->subproduct;
}
/**
* Sets the subproduct
*
* @param bool $subproduct
* @return void
*/
public function setSubproduct($subproduct)
{
$this->subproduct = $subproduct;
}
/**
* Initializes the ObjectStorage for categories
*/
public function __construct()
{
$this->categories = new ObjectStorage();
$this->productimage = new ObjectStorage();
$this->downloads = new ObjectStorage();
}
/**
* Adds a category
*
* @param Category $category
* @return void
*/
public function addCategory(Category $category)
{
$this->categories->attach($category);
}
/**
* Removes a category
*
* @param Category $categoryToRemove
* @return void
*/
public function removeCategory(Category $categoryToRemove)
{
$this->categories->detach($categoryToRemove);
}
/**
* Returns the categories
*
* @return ObjectStorage<Category>
*/
public function getCategories()
{
return $this->categories;
}
/**
* Sets the categories
*
* @param ObjectStorage<Category> $categories
* @return void
*/
public function setCategories(ObjectStorage $categories)
{
$this->categories = $categories;
}
/**
* Adds a product image
*
* @param FileReference $productimage
* @return void
*/
public function addProductimage(FileReference $productimage)
{
$this->productimage->attach($productimage);
}
/**
* Removes a product image
*
* @param FileReference $productimageToRemove
* @return void
*/
public function removeProductimage(FileReference $productimageToRemove)
{
$this->productimage->detach($productimageToRemove);
}
/**
* Returns the product images
*
* @return ObjectStorage<FileReference>
*/
public function getProductimage()
{
return $this->productimage;
}
/**
* Sets the product images
*
* @param ObjectStorage<FileReference> $productimage
* @return void
*/
public function setProductimage(ObjectStorage $productimage)
{
$this->productimage = $productimage;
}
/**
* Adds a download
*
* @param \Evomedien\Vitec\Domain\Model\Download $download
* @return void
*/
public function addDownload(\Evomedien\Vitec\Domain\Model\Download $download): void
{
$this->downloads->attach($download);
}
/**
* Removes a download
*
* @param \Evomedien\Vitec\Domain\Model\Download $downloadToRemove
* @return void
*/
public function removeDownload(\Evomedien\Vitec\Domain\Model\Download $downloadToRemove): void
{
$this->downloads->detach($downloadToRemove);
}
/**
* Returns the downloads
*
* @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
*/
public function getDownloads(): ObjectStorage
{
return $this->downloads;
}
/**
* Sets the downloads
*
* @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $downloads
* @return void
*/
public function setDownloads(ObjectStorage $downloads): void
{
$this->downloads = $downloads;
}
/**
* Returns the Open Graph Image
*
* @return FileReference|null
*/
public function getOgimage(): ?FileReference
{
return $this->ogimage;
}
/**
* Sets the Open Graph Image
*
* @param FileReference $ogimage
* @return void
*/
public function setOgimage(FileReference $ogimage): void
{
$this->ogimage = $ogimage;
}
/**
* Returns the content element link
*
* @return string
*/
/**
* Returns the uploaded video file
*
* @return \TYPO3\CMS\Extbase\Domain\Model\FileReference|null
*/
public function getVideofile(): ?\TYPO3\CMS\Extbase\Domain\Model\FileReference
{
return $this->videofile;
}
/**
* Sets the uploaded video file
*
* @param \TYPO3\CMS\Extbase\Domain\Model\FileReference|null $videofile
* @return void
*/
public function setVideofile(?\TYPO3\CMS\Extbase\Domain\Model\FileReference $videofile): void
{
$this->videofile = $videofile;
}
public function getContentelement(): string
{
return $this->contentelement;
}
/**
* Sets the content element link
*
* @param string $contentelement
* @return void
*/
public function setContentelement(string $contentelement): void
{
$this->contentelement = $contentelement;
}
/**
* Returns the content element cta link
*
* @return string
*/
public function getContentelementcta(): string
{
return $this->contentelementcta;
}
/**
* Sets the content element link cta
*
* @param string $contentelementcta
* @return void
*/
public function setContentelementcta(string $contentelementcta): void
{
$this->contentelementcta = $contentelementcta;
}
/**
* @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product>
*/
public function getRelatedprodukt(): \TYPO3\CMS\Extbase\Persistence\ObjectStorage
{
return $this->relatedprodukt;
}
/**
* @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Vendor\Extension\Domain\Model\Product> $relatedprodukt
*/
public function setRelatedprodukt(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $relatedprodukt): void
{
$this->relatedprodukt = $relatedprodukt;
}
/* --------------------------------------------------------------------- */
}

View File

0
packages/vitec/Classes/Hook/SyncBackendLayoutHook.php Executable file → Normal file
View File

View File

@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -68,10 +68,12 @@ final class ContentElementResolver
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms:
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (page links etc.)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
@@ -155,33 +157,47 @@ final class ContentElementResolver
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
if (!str_starts_with($link, 't3://record')) {
return 0;
}
$parts = parse_url($link);
if (empty($parts['query'])) {
if (!is_array($parts)) {
return 0;
}
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier !== 'tt_content' || $uid <= 0) {
return 0;
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
return $uid;
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**

View File

@@ -0,0 +1,256 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
$listType = (string)($record['list_type'] ?? '');
if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolves a TYPO3 typolink string (as stored by `inputLink` fields like
* Product.contentelement / Product.contentelementcta) to the JSON
* representation of the referenced tt_content element.
*
* Mirrors the shape produced by
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}:
* { id, type, colPos, sorting, appearance, data }
*
* Nested VITEC list-plugins are resolved through their `renderForRecord()`
* just like container children, so a referenced productlist / productshow /
* usecaselist / usecaseshow appears with its full headless JSON in `data`.
*
* Exception-safe: every public entry point returns `null` on any failure
* so the surrounding JSON output stays clean.
*/
final class ContentElementResolver
{
/** 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 = [
'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',
'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',
];
private const KEEP_IF_ZERO = ['header_layout'];
private const PLUGIN_RENDERERS = [
'vitec_productlist' => [ProductListJsonRenderer::class, 'products'],
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
];
/**
* Resolve a typolink string to a normalised tt_content JSON element.
*
* Accepted forms (all may include an optional anchor fragment):
* - "t3://record?identifier=tt_content&uid=N"
* - "t3://page?uid=PAGE#N" (link-popup: pick a CE on a page;
* the fragment is the tt_content uid)
* - "<numeric>" (legacy: bare tt_content uid)
* - everything else → null (pure page links etc.)
*
* @return array<string,mixed>|null
*/
public static function resolveLink(?string $link): ?array
{
try {
$link = trim((string)$link);
if ($link === '') {
return null;
}
$uid = self::extractTtContentUid($link);
if ($uid <= 0) {
return null;
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$row = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return self::normaliseRecord($row);
} catch (\Throwable $e) {
return null;
}
}
/**
* Normalise a tt_content DB row to the same envelope shape that
* {@see \Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor}
* emits for container children. VITEC list-plugin children are
* resolved to their headless JSON.
*
* @param array<string,mixed> $record
* @return array<string,mixed>
*/
public static function normaliseRecord(array $record): array
{
$data = [];
foreach ($record as $field => $value) {
if (in_array($field, self::ENVELOPE, true)) {
continue;
}
if (in_array($field, self::SYSTEM_FIELDS, true)) {
continue;
}
if (self::isEmpty($value) && !in_array($field, self::KEEP_IF_ZERO, true)) {
continue;
}
$data[$field] = self::castValue($field, $value);
}
self::resolvePluginData($record, $data);
return [
'id' => (int)$record['uid'],
'type' => (string)$record['CType'],
'colPos' => (int)($record['colPos'] ?? 0),
'sorting' => (int)($record['sorting'] ?? 0),
'appearance' => [
'layout' => (string)($record['layout'] ?? ''),
'frameClass' => (string)($record['frame_class'] ?? 'default'),
'spaceBefore' => (string)($record['space_before_class'] ?? ''),
'spaceAfter' => (string)($record['space_after_class'] ?? ''),
],
'data' => (object)$data,
];
}
/**
* Extract the tt_content uid from a typolink string.
*
* Handles four forms:
* 1. plain numeric → tt_content uid
* 2. t3://record?identifier=tt_content&uid=N → uid N
* 3. t3://record?identifier=tt_content&uid=N#X → uid N (anchor ignored)
* 4. t3://page?uid=PAGE#N → uid N from fragment
* (link-popup picks a content element on a page; the fragment is
* the tt_content uid, the query is the page uid)
*/
private static function extractTtContentUid(string $link): int
{
// Form 1: bare numeric uid
if (ctype_digit($link)) {
return (int)$link;
}
$parts = parse_url($link);
if (!is_array($parts)) {
return 0;
}
// Forms 2 & 3: t3://record?identifier=tt_content&uid=N
if (str_starts_with($link, 't3://record') && !empty($parts['query'])) {
$params = [];
parse_str((string)$parts['query'], $params);
$identifier = (string)($params['identifier'] ?? '');
$uid = (int)($params['uid'] ?? 0);
if ($identifier === 'tt_content' && $uid > 0) {
return $uid;
}
}
// Form 4: t3://page?uid=PAGE#N → tt_content uid lives in the fragment
if (str_starts_with($link, 't3://page')) {
$fragment = (string)($parts['fragment'] ?? '');
if (ctype_digit($fragment)) {
return (int)$fragment;
}
}
return 0;
}
/**
* If the record is a VITEC list-plugin, run its renderer for THIS row
* and inject the decoded result under its key. Drops raw pi_flexform.
*
* @param array<string,mixed> $record
* @param array<string,mixed> $data
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {
return;
}
$json = $renderer->renderForRecord($record);
if ($json === '' || $json === null) {
return;
}
$decoded = json_decode($json, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return;
}
$data[$jsonKey] = $decoded;
unset($data['pi_flexform']);
} catch (\Throwable $e) {
// leave raw data on failure
}
}
private static function isEmpty(mixed $value): bool
{
return $value === null || $value === '' || $value === 0 || $value === '0';
}
private static function castValue(string $field, mixed $value): mixed
{
if (in_array($field, ['header_layout'], true)) {
return (int)$value;
}
return $value;
}
}

View File

@@ -9,6 +9,11 @@ use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
use Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer;
use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -63,6 +68,11 @@ final class ContentElementResolver
'vitec_productshow' => [ProductShowJsonRenderer::class, 'product'],
'vitec_usecaselist' => [UsecaseListJsonRenderer::class, 'usecases'],
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
'vitec_datasheets' => [DatasheetsJsonRenderer::class, 'datasheets'],
];
/**
@@ -209,13 +219,17 @@ final class ContentElementResolver
*/
private static function resolvePluginData(array $record, array &$data): void
{
// v14: plugins are their own CType; legacy elements still carry list_type.
$cType = (string)($record['CType'] ?? '');
$listType = (string)($record['list_type'] ?? '');
if ($listType === '' || !isset(self::PLUGIN_RENDERERS[$listType])) {
$key = isset(self::PLUGIN_RENDERERS[$cType]) ? $cType
: (isset(self::PLUGIN_RENDERERS[$listType]) ? $listType : null);
if ($key === null) {
return;
}
try {
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$listType];
[$rendererClass, $jsonKey] = self::PLUGIN_RENDERERS[$key];
$renderer = GeneralUtility::makeInstance($rendererClass);
if (!method_exists($renderer, 'renderForRecord')) {

View File

@@ -0,0 +1,395 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
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 Datasheets plugin as JSON.
*
* Lists every product that has at least one datasheet, with the LATEST
* datasheet attached. "Datasheet" = a Download linked to the product via
* tx_vitec_product_download_mm with hideondatasheets = 0. "Latest" is
* the download with the highest tstamp.
*
* The plugin is global — the page on which it is placed is not used as
* a filter. FlexForm settings (showFilter, showSearch, itemsPerPage)
* are passed through for the frontend UI to honour.
*
* Exception-safe. Returns '' on failure / nothing to render.
*/
class DatasheetsJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = 0;
$req = $GLOBALS["TYPO3_REQUEST"] ?? null;
if ($req !== null) {
$pi = $req->getAttribute("frontend.page.information");
if ($pi !== null) { $pageId = (int)$pi->getId(); }
}
if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); }
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$rows = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
$qb->expr()->or(
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_datasheets', ParameterType::STRING)),
$qb->expr()->and(
$qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)),
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_datasheets', ParameterType::STRING))
)
),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
if (empty($rows)) {
return '';
}
return $this->renderForRecord($rows[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);
$settingsOut = [
'showFilter' => (bool)($settings['showFilter'] ?? true),
'showSearch' => (bool)($settings['showSearch'] ?? true),
'itemsPerPage' => (int)($settings['itemsPerPage'] ?? 20),
];
// 1) Pull all (product_uid, latest_datasheet_uid) pairs in one query.
// GROUP BY product, take MAX(tstamp) → join back to get that exact download.
$items = $this->fetchProductLatestDatasheetPairs();
// 2) Hydrate each pair: full product + full datasheet objects.
$itemsOut = [];
foreach ($items as $pair) {
$productUid = (int)$pair['product_uid'];
$datasheetUid = (int)$pair['datasheet_uid'];
$product = $this->fetchProduct($productUid);
$datasheet = $this->fetchDownload($datasheetUid);
if (!$product || !$datasheet) {
continue;
}
$itemsOut[] = [
'product' => $this->serializeProduct($product),
'datasheet' => $this->serializeDatasheet($datasheet),
];
}
$response = [
'items' => $itemsOut,
'settings' => $settingsOut,
];
if ($debugMode) {
$response['debug'] = [
'productCount' => count($itemsOut),
'settings' => $settings,
];
}
return json_encode($response);
} catch (\Throwable $e) {
return '';
}
}
/**
* For each visible product that has at least one visible datasheet,
* return [product_uid => latest_datasheet_uid]. Sorted by product title.
*
* @return array<int,array{product_uid:int,datasheet_uid:int}>
*/
private function fetchProductLatestDatasheetPairs(): array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_product_download_mm');
$expr = $qb->expr();
// Inner aggregation: for each product, the latest download tstamp.
// We use a single query with JOIN on the same (mm + download) to resolve
// the actual download_uid matching MAX(tstamp). Approach: do it in two
// simple steps to stay SQL-portable.
// Step A: gather all candidate (product_uid, download_uid, tstamp) tuples.
$rows = $qb
->select(
'mm.uid_local AS product_uid',
'mm.uid_foreign AS datasheet_uid',
'd.tstamp AS d_tstamp',
'p.title AS p_title'
)
->from('tx_vitec_product_download_mm', 'mm')
->join('mm', 'tx_vitec_domain_model_download', 'd', 'd.uid = mm.uid_foreign')
->join('mm', 'tx_vitec_domain_model_product', 'p', 'p.uid = mm.uid_local')
->where(
$expr->eq('d.deleted', 0),
$expr->eq('d.hidden', 0),
$expr->eq('d.hideonwebsite', 0),
$expr->eq('d.hideondatasheets', 0),
$expr->eq('p.deleted', 0),
$expr->eq('p.hidden', 0),
$expr->eq('p.hideonwebsite', 0)
)
->executeQuery()
->fetchAllAssociative();
// Step B: reduce to {product_uid => latest datasheet} in PHP.
$latestPerProduct = []; // product_uid => [datasheet_uid, tstamp, title]
foreach ($rows as $r) {
$pUid = (int)$r['product_uid'];
$dUid = (int)$r['datasheet_uid'];
$ts = (int)$r['d_tstamp'];
$title = (string)$r['p_title'];
if (!isset($latestPerProduct[$pUid]) || $ts > $latestPerProduct[$pUid]['tstamp']) {
$latestPerProduct[$pUid] = [
'product_uid' => $pUid,
'datasheet_uid' => $dUid,
'tstamp' => $ts,
'title' => $title,
];
}
}
// Sort by product title ASC.
usort($latestPerProduct, static fn($a, $b) => strcasecmp($a['title'], $b['title']));
// Strip helper fields.
return array_map(
static fn($r) => ['product_uid' => $r['product_uid'], 'datasheet_uid' => $r['datasheet_uid']],
$latestPerProduct
);
}
/**
* @return array<string,mixed>|false
*/
private function fetchProduct(int $uid)
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_product');
return $qb
->select('uid', 'title', 'slug', 'subtitle', 'teaser')
->from('tx_vitec_domain_model_product')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
}
/**
* @return array<string,mixed>|false
*/
private function fetchDownload(int $uid)
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_download');
return $qb
->select('*')
->from('tx_vitec_domain_model_download')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
}
/**
* @param array<string,mixed> $product
* @return array<string,mixed>
*/
private function serializeProduct(array $product): array
{
$uid = (int)$product['uid'];
return [
'uid' => $uid,
'title' => (string)($product['title'] ?? ''),
'slug' => (string)($product['slug'] ?? ''),
'subtitle' => (string)($product['subtitle'] ?? ''),
'teaser' => (string)($product['teaser'] ?? ''),
'link' => '/product/' . (string)($product['slug'] ?? ''),
'image' => $this->getProductFirstImage($uid),
'categories' => $this->getProductCategories($uid),
];
}
/**
* @param array<string,mixed> $download
* @return array<string,mixed>
*/
private function serializeDatasheet(array $download): array
{
$uid = (int)$download['uid'];
return [
'uid' => $uid,
'title' => (string)($download['title'] ?? ''),
'slug' => (string)($download['slug'] ?? ''),
'teaser' => (string)($download['teaser'] ?? ''),
'description' => (string)($download['description'] ?? ''),
'tstamp' => (int)($download['tstamp'] ?? 0),
'file' => $this->getDownloadFile($uid),
];
}
/**
* @return array<string,mixed>|null
*/
private function getProductFirstImage(int $productUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$fileRefData = $qb
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
->from('sys_file_reference', 'sfr')
->where(
$qb->expr()->eq('sfr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
$qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter('productimage', ParameterType::STRING)),
$qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($productUid, ParameterType::INTEGER)),
$qb->expr()->eq('sfr.deleted', 0),
$qb->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']);
$default = $imageService->applyProcessingInstructions(
$fileReference,
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
);
return [
'uid' => (int)$fileRefData['uid'],
'url' => $imageService->getImageUri($default),
'title' => $fileRefData['title'] ?? '',
'alternative' => $fileRefData['alternative'] ?? '',
'description' => $fileRefData['description'] ?? '',
];
} catch (\Exception $e) {
return null;
}
}
private function getProductCategories(int $productUid): array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$categories = $qb
->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 = ' .
$qb->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) .
' AND mm.fieldname = ' .
$qb->createNamedParameter('categories', ParameterType::STRING)
)
->where(
$qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($productUid, ParameterType::INTEGER)),
$qb->expr()->eq('c.deleted', 0),
$qb->expr()->eq('c.hidden', 0)
)
->orderBy('mm.sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
return array_map(static function ($cat) {
return [
'uid' => (int)$cat['uid'],
'title' => (string)($cat['title'] ?? ''),
'description' => (string)($cat['description'] ?? ''),
];
}, $categories);
}
/**
* @return array<string,mixed>|null
*/
private function getDownloadFile(int $downloadUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $qb
->select('fr.uid', 'fr.title', 'fr.description', '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(
$qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
$qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)),
$qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)),
$qb->expr()->eq('fr.deleted', 0),
$qb->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* UserFunc to render the Downloadcard plugin as JSON for headless output.
*
* Behaviour:
* - settings.download set -> single download object (key: "download")
* - else settings.product set -> all downloads of that product (key: "downloads",
* plus a small "product" context)
* - neither -> empty (key removed by headless ifEmptyUnsetKey)
*
* `settings.layout` / magstyle / magheader / magtext / maglink / adddetaillink
* are always passed through under the "settings" block for the frontend.
*
* `render()` performs page discovery; `renderForRecord()` processes a specific
* tt_content row and is reused by ContainerChildrenProcessor /
* ContentElementResolver. Exception-safe.
*/
class DownloadcardJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = 0;
$req = $GLOBALS["TYPO3_REQUEST"] ?? null;
if ($req !== null) {
$pi = $req->getAttribute("frontend.page.information");
if ($pi !== null) { $pageId = (int)$pi->getId(); }
}
if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); }
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
// After v14 migration the CType is the plugin signature; for legacy
// (unmigrated) records the CType is still "list" with list_type set.
$rows = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
$qb->expr()->or(
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING)),
$qb->expr()->and(
$qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)),
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING))
)
),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
if (empty($rows)) {
return '';
}
return $this->renderForRecord($rows[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'] ?? [];
$downloadUid = (int)($settings['download'] ?? 0);
$productUid = (int)($settings['product'] ?? 0);
$debugMode = (bool)($settings['debug'] ?? false);
// Settings always carried through to the frontend
$settingsOut = [
'layout' => (string)($settings['layout'] ?? ''),
'magstyle' => (bool)($settings['magstyle'] ?? false),
'magheader' => (string)($settings['magheader'] ?? ''),
'magtext' => (string)($settings['magtext'] ?? ''),
'maglink' => (string)($settings['maglink'] ?? ''),
'adddetaillink' => (bool)($settings['adddetaillink'] ?? false),
];
// --- Mode 1: single download ---
if ($downloadUid > 0) {
$download = $this->fetchDownload($downloadUid);
if (!$download) {
return $debugMode
? json_encode(['error' => 'Download not found', 'debug' => ['downloadUid' => $downloadUid]])
: '';
}
$response = [
'mode' => 'single',
'download' => $this->serializeDownload($download),
'settings' => $settingsOut,
];
if ($debugMode) {
$response['debug'] = [
'pageId' => $pageId, 'downloadUid' => $downloadUid, 'settings' => $settings,
];
}
return json_encode($response);
}
// --- Mode 2: all downloads of a product ---
if ($productUid > 0) {
$downloads = $this->fetchDownloadsForProduct($productUid);
$product = $this->fetchProductContext($productUid);
$response = [
'mode' => 'product-downloads',
'product' => $product,
'downloads' => array_map(fn($r) => $this->serializeDownload($r), $downloads),
'settings' => $settingsOut,
];
if ($debugMode) {
$response['debug'] = [
'pageId' => $pageId,
'productUid' => $productUid,
'downloadCount' => count($downloads),
'settings' => $settings,
];
}
return json_encode($response);
}
// Neither selected: nothing to render.
return $debugMode
? json_encode(['error' => 'No download or product selected', 'debug' => ['settings' => $settings]])
: '';
} catch (\Throwable $e) {
return '';
}
}
/**
* @return array<string,mixed>|false
*/
private function fetchDownload(int $uid)
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_download');
return $qb
->select('*')
->from('tx_vitec_domain_model_download')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0),
$qb->expr()->eq('hideonwebsite', 0)
)
->executeQuery()
->fetchAssociative();
}
/**
* @return array<int,array<string,mixed>>
*/
private function fetchDownloadsForProduct(int $productUid): array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_download');
return $qb
->select('d.*')
->from('tx_vitec_domain_model_download', 'd')
->join('d', 'tx_vitec_product_download_mm', 'mm', 'mm.uid_foreign = d.uid')
->where(
$qb->expr()->eq('mm.uid_local', $qb->createNamedParameter($productUid, ParameterType::INTEGER)),
$qb->expr()->eq('d.deleted', 0),
$qb->expr()->eq('d.hidden', 0),
$qb->expr()->eq('d.hideonwebsite', 0)
)
->orderBy('mm.sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
/**
* @return array<string,mixed>|null
*/
private function fetchProductContext(int $productUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_product');
$row = $qb
->select('uid', 'title', 'slug', 'subtitle', 'teaser')
->from('tx_vitec_domain_model_product')
->where(
$qb->expr()->eq('uid', $qb->createNamedParameter($productUid, ParameterType::INTEGER)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['uid'],
'title' => (string)($row['title'] ?? ''),
'slug' => (string)($row['slug'] ?? ''),
'subtitle' => (string)($row['subtitle'] ?? ''),
'teaser' => (string)($row['teaser'] ?? ''),
'link' => '/product/' . (string)($row['slug'] ?? ''),
];
}
/**
* @param array<string,mixed> $download
* @return array<string,mixed>
*/
protected function serializeDownload(array $download): array
{
$uid = (int)$download['uid'];
return [
'uid' => $uid,
'title' => (string)($download['title'] ?? ''),
'slug' => (string)($download['slug'] ?? ''),
'teaser' => (string)($download['teaser'] ?? ''),
'description' => (string)($download['description'] ?? ''),
'keywords' => (string)($download['keywords'] ?? ''),
'icon' => (string)($download['icon'] ?? ''),
'filepath' => (string)($download['filepath'] ?? ''),
'fileprefix' => (string)($download['fileprefix'] ?? ''),
'private_download' => (bool)($download['private_download'] ?? false),
'hideonapp' => (bool)($download['hideonapp'] ?? false),
'hideonwebsite' => (bool)($download['hideonwebsite'] ?? false),
'hideondatasheets' => (bool)($download['hideondatasheets'] ?? false),
'hideonproducts' => (bool)($download['hideonproducts'] ?? false),
'file' => $this->getDownloadFile($uid),
];
}
/**
* Resolve the FAL file reference (fieldname=file) for a download.
*
* @return array<string,mixed>|null
*/
protected function getDownloadFile(int $downloadUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $qb
->select('fr.uid', 'fr.title', 'fr.description', '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(
$qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
$qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)),
$qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)),
$qb->expr()->eq('fr.deleted', 0),
$qb->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
}

View File

@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
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 Downloadcardcollection plugin as JSON.
*
* FlexForm-driven (no controller action):
* - settings.download : comma-separated tx_vitec_domain_model_download UIDs
* (selectMultipleSideBySide).
* - settings.image : optional header image (inline FAL on the
* tt_content row, fieldname=image).
* - settings.layout / magstyle / magheader / magtext / maglink / adddetaillink
* are passed through under "settings".
*
* Exception-safe. Returns '' when nothing to render.
*/
class DownloadcardcollectionJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = 0;
$req = $GLOBALS["TYPO3_REQUEST"] ?? null;
if ($req !== null) {
$pi = $req->getAttribute("frontend.page.information");
if ($pi !== null) { $pageId = (int)$pi->getId(); }
}
if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); }
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$rows = $qb
->select('*')
->from('tt_content')
->where(
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
$qb->expr()->or(
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcardcollection', ParameterType::STRING)),
$qb->expr()->and(
$qb->expr()->eq('CType', $qb->createNamedParameter('list', ParameterType::STRING)),
$qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcardcollection', ParameterType::STRING))
)
),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
if (empty($rows)) {
return '';
}
return $this->renderForRecord($rows[0]);
}
/**
* @param array<string,mixed> $contentElement
*/
public function renderForRecord(array $contentElement): string
{
try {
$ttContentUid = (int)($contentElement['uid'] ?? 0);
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
$settings = $flexFormData['settings'] ?? [];
$debugMode = (bool)($settings['debug'] ?? false);
// Pass-through settings for the frontend
$settingsOut = [
'layout' => (string)($settings['layout'] ?? ''),
'magstyle' => (bool)($settings['magstyle'] ?? false),
'magheader' => (string)($settings['magheader'] ?? ''),
'magtext' => (string)($settings['magtext'] ?? ''),
'maglink' => (string)($settings['maglink'] ?? ''),
'adddetaillink' => (bool)($settings['adddetaillink'] ?? false),
];
// --- Downloads: comma-separated UID list from settings.download ---
$downloadUids = $this->parseUidList($settings['download'] ?? '');
$downloads = $downloadUids === [] ? [] : $this->fetchDownloadsByUids($downloadUids);
// --- Header image (inline FAL on this tt_content element, fieldname=image) ---
$image = $this->getCollectionImage($ttContentUid);
$response = [
'image' => $image,
'downloads' => array_map(fn($r) => $this->serializeDownload($r), $downloads),
'settings' => $settingsOut,
];
if ($debugMode) {
$response['debug'] = [
'ttContentUid' => $ttContentUid,
'downloadUids' => $downloadUids,
'downloadCount' => count($downloads),
'settings' => $settings,
];
}
return json_encode($response);
} catch (\Throwable $e) {
return '';
}
}
/**
* Accepts "5,7,12" or "5,,7" or even an array-like value. Returns int[].
*
* @return int[]
*/
private function parseUidList(mixed $raw): array
{
if (is_array($raw)) {
$raw = implode(',', $raw);
}
$raw = (string)$raw;
if ($raw === '') {
return [];
}
$uids = array_filter(array_map('intval', explode(',', $raw)), static fn(int $u) => $u > 0);
return array_values(array_unique($uids));
}
/**
* @param int[] $uids
* @return array<int,array<string,mixed>>
*/
private function fetchDownloadsByUids(array $uids): array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_download');
$rows = $qb
->select('*')
->from('tx_vitec_domain_model_download')
->where(
$qb->expr()->in('uid', $qb->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0),
$qb->expr()->eq('hideonwebsite', 0)
)
->executeQuery()
->fetchAllAssociative();
// Preserve the editor-defined order from $uids
$orderMap = array_flip($uids);
usort($rows, static function ($a, $b) use ($orderMap) {
return ($orderMap[(int)$a['uid']] ?? PHP_INT_MAX) <=> ($orderMap[(int)$b['uid']] ?? PHP_INT_MAX);
});
return $rows;
}
/**
* Resolve the inline FAL header image stored on this tt_content row
* (tablenames='tt_content', fieldname='image').
*
* @return array<string,mixed>|null
*/
private function getCollectionImage(int $ttContentUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$fileRefData = $qb
->select('sfr.uid', 'sfr.title', 'sfr.description', 'sfr.alternative', 'sfr.crop')
->from('sys_file_reference', 'sfr')
->where(
$qb->expr()->eq('sfr.tablenames', $qb->createNamedParameter('tt_content', ParameterType::STRING)),
$qb->expr()->eq('sfr.fieldname', $qb->createNamedParameter('image', ParameterType::STRING)),
$qb->expr()->eq('sfr.uid_foreign', $qb->createNamedParameter($ttContentUid, ParameterType::INTEGER)),
$qb->expr()->eq('sfr.deleted', 0),
$qb->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;
}
}
/**
* @param array<string,mixed> $download
* @return array<string,mixed>
*/
protected function serializeDownload(array $download): array
{
$uid = (int)$download['uid'];
return [
'uid' => $uid,
'title' => (string)($download['title'] ?? ''),
'slug' => (string)($download['slug'] ?? ''),
'teaser' => (string)($download['teaser'] ?? ''),
'description' => (string)($download['description'] ?? ''),
'keywords' => (string)($download['keywords'] ?? ''),
'icon' => (string)($download['icon'] ?? ''),
'filepath' => (string)($download['filepath'] ?? ''),
'fileprefix' => (string)($download['fileprefix'] ?? ''),
'private_download' => (bool)($download['private_download'] ?? false),
'hideonapp' => (bool)($download['hideonapp'] ?? false),
'hideonwebsite' => (bool)($download['hideonwebsite'] ?? false),
'hideondatasheets' => (bool)($download['hideondatasheets'] ?? false),
'hideonproducts' => (bool)($download['hideonproducts'] ?? false),
'file' => $this->getDownloadFile($uid),
];
}
/**
* Resolve the FAL file reference (fieldname=file) for a download.
*
* @return array<string,mixed>|null
*/
protected function getDownloadFile(int $downloadUid): ?array
{
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $qb
->select('fr.uid', 'fr.title', 'fr.description', '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(
$qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)),
$qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)),
$qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)),
$qb->expr()->eq('fr.deleted', 0),
$qb->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
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 market as JSON for headless output.
*
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
* one specific tt_content row, reused by ContainerChildrenProcessor and
* ContentElementResolver. Exception-safe.
*/
class MarketShowJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_marketshow') {
return $this->renderForRecord($row);
}
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$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('CType', $queryBuilder->createNamedParameter('vitec_marketshow', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->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'] ?? [];
$marketUid = (int)($settings['market'] ?? 0);
$layout = (string)($settings['layout'] ?? 'default');
$debugMode = (bool)($settings['debug'] ?? false);
// Fallback: route / query parameter (numeric uid only — Market has no slug)
if (!$marketUid) {
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
$marketParam = $routeParams['tx_vitec_marketshow']['market'] ?? null;
if ($marketParam && is_numeric($marketParam)) {
$marketUid = (int)$marketParam;
}
}
if (!$marketUid) {
return $debugMode
? json_encode(['error' => 'No market selected or found', 'debug' => ['settings' => $settings]])
: '';
}
$marketQb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_market');
$market = $marketQb
->select('*')
->from('tx_vitec_domain_model_market')
->where(
$marketQb->expr()->eq('uid', $marketQb->createNamedParameter($marketUid, ParameterType::INTEGER)),
$marketQb->expr()->eq('deleted', 0),
$marketQb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$market) {
return $debugMode
? json_encode(['error' => 'Market not found', 'debug' => ['marketUid' => $marketUid]])
: '';
}
$response = [
'market' => $this->serializeMarket($market),
'layout' => $layout,
'settings' => [
'layout' => $layout,
],
];
if ($debugMode) {
$response['debug'] = [
'pageId' => $pageId,
'marketUid' => $marketUid,
'layout' => $layout,
'settings' => $settings,
];
}
return json_encode($response);
} catch (\Throwable $e) {
return '';
}
}
/**
* @param array<string,mixed> $market
* @return array<string,mixed>
*/
protected function serializeMarket(array $market): array
{
$uid = (int)$market['uid'];
return [
'uid' => $uid,
'title' => (string)($market['title'] ?? ''),
'subtitle' => (string)($market['subtitle'] ?? ''),
'teaser' => (string)($market['teaser'] ?? ''),
'description' => (string)($market['description'] ?? ''),
'categories' => $this->getMarketCategories($uid),
'image' => $this->getMarketImage($uid),
];
}
/**
* Resolve the single image FAL reference for a market (fieldname=image).
*
* @return array<string,mixed>|null
*/
protected function getMarketImage(int $marketUid): ?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_market', ParameterType::STRING)),
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('image', ParameterType::STRING)),
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($marketUid, 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 getMarketCategories(int $marketUid): 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_market', ParameterType::STRING) .
' AND mm.fieldname = ' .
$queryBuilder->createNamedParameter('categories', ParameterType::STRING)
)
->where(
$queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($marketUid, 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);
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use Doctrine\DBAL\ParameterType;
use Evomedien\Vitec\Domain\Repository\ProductRepository;
use Psr\Http\Message\ServerRequestInterface;
@@ -25,9 +27,30 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class ProductListJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productlist') {
return $this->renderForRecord($row);
}
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
@@ -37,7 +60,7 @@ class ProductListJsonRenderer
->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('CType', $queryBuilder->createNamedParameter('vitec_productlist', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
@@ -45,13 +68,13 @@ class ProductListJsonRenderer
->fetchAllAssociative();
if (empty($contentElements)) {
// Not a product-list page: emit nothing so headless removes the key.
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-list plugin element).
* Exception-safe: returns '' on any failure.

View File

@@ -1,228 +0,0 @@
<?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) {
// Get product images (FAL references)
$images = $this->getProductImages((int)$product['uid']);
$productsData[] = [
'uid' => (int)$product['uid'],
'title' => $product['title'] ?? '',
'subtitle' => $product['subtitle'] ?? '',
'slug' => $product['slug'] ?? '',
'teaser' => $product['teaser'] ?? '',
'description' => $product['description'] ?? '',
'link' => '/product/' . ($product['slug'] ?? ''),
'images' => $images,
// Add more fields as needed
];
}
// 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);
}
/**
* 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;
}
}

View File

@@ -1,499 +0,0 @@
<?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;
}
}

View File

@@ -1,463 +0,0 @@
<?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.
*
* `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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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)) {
// Not a product-list page: emit nothing so headless removes the key.
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-list plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$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 — direct query.
$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)
);
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 '';
}
}
/**
* Serialize a single product DB row to the full headless JSON structure.
*
* @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
{
$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 {
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
$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'
];
}
$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) {
continue;
}
}
return $images;
}
/**
* @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;
}
}
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);
}
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;
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;
}
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'];
$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;
}
}

View File

@@ -1,507 +0,0 @@
<?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.
*
* `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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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)) {
// Not a product-list page: emit nothing so headless removes the key.
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-list plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$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 — direct query.
$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)
);
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 '';
}
}
/**
* Serialize a single product DB row to the full headless JSON structure.
*
* @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),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
];
}
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 {
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
$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'
];
}
$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) {
continue;
}
}
return $images;
}
/**
* @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;
}
}
/**
* Resolve the uploaded video file (single FAL reference, fieldname=videofile).
*
* @return array<string,mixed>|null
*/
protected function getProductVideoFile(int $productUid): ?array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $queryBuilder
->select('fr.uid', 'fr.title', 'fr.description', '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(
$queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('fr.deleted', 0),
$queryBuilder->expr()->eq('fr.hidden', 0),
$queryBuilder->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
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);
}
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;
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;
}
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'];
$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;
}
}

View File

@@ -1,507 +0,0 @@
<?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.
*
* `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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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)) {
// Not a product-list page: emit nothing so headless removes the key.
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-list plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$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 — direct query.
$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)
);
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 '';
}
}
/**
* Serialize a single product DB row to the full headless JSON structure.
*
* @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' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')),
'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((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),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
];
}
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 {
$fileReference = $resourceFactory->getFileReferenceObject((int)$fileRefData['uid']);
$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'
];
}
$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) {
continue;
}
}
return $images;
}
/**
* @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;
}
}
/**
* Resolve the uploaded video file (single FAL reference, fieldname=videofile).
*
* @return array<string,mixed>|null
*/
protected function getProductVideoFile(int $productUid): ?array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $queryBuilder
->select('fr.uid', 'fr.title', 'fr.description', '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(
$queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('fr.deleted', 0),
$queryBuilder->expr()->eq('fr.hidden', 0),
$queryBuilder->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
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);
}
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;
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;
}
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'];
$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;
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -23,11 +25,32 @@ use Doctrine\DBAL\ParameterType;
*/
class ProductShowJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productshow') {
return $this->renderForRecord($row);
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$contentElements = $queryBuilder
@@ -35,7 +58,7 @@ class ProductShowJsonRenderer
->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('CType', $queryBuilder->createNamedParameter('vitec_productshow', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
@@ -49,6 +72,7 @@ class ProductShowJsonRenderer
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-show plugin element).
* Exception-safe: returns '' on any failure.

View File

@@ -1,366 +0,0 @@
<?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;
}
}

View File

@@ -1,512 +0,0 @@
<?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;
}
}

View File

@@ -1,470 +0,0 @@
<?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 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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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();
if (empty($contentElements)) {
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-show plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$productUid = (int)($settings['product'] ?? 0);
$layout = (int)($settings['layout'] ?? 0);
$debugMode = (bool)($settings['debug'] ?? false);
if (!$productUid) {
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
if ($productParam) {
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 $debugMode
? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]])
: '';
}
$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 $debugMode
? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]])
: '';
}
$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);
} catch (\Throwable $e) {
return '';
}
}
/**
* @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
{
$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();
$processedImage = $imageService->applyProcessingInstructions(
$fileReference,
['width' => '1874c', 'height' => '625c']
);
$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) {
continue;
}
}
return $images;
}
/**
* @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
{
$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);
}
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;
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;
}
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;
}
}

View File

@@ -1,514 +0,0 @@
<?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 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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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();
if (empty($contentElements)) {
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-show plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$productUid = (int)($settings['product'] ?? 0);
$layout = (int)($settings['layout'] ?? 0);
$debugMode = (bool)($settings['debug'] ?? false);
if (!$productUid) {
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
if ($productParam) {
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 $debugMode
? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]])
: '';
}
$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 $debugMode
? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]])
: '';
}
$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);
} catch (\Throwable $e) {
return '';
}
}
/**
* @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),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
];
}
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();
$processedImage = $imageService->applyProcessingInstructions(
$fileReference,
['width' => '1874c', 'height' => '625c']
);
$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) {
continue;
}
}
return $images;
}
/**
* @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;
}
}
/**
* Resolve the uploaded video file (single FAL reference, fieldname=videofile).
*
* @return array<string,mixed>|null
*/
protected function getProductVideoFile(int $productUid): ?array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $queryBuilder
->select('fr.uid', 'fr.title', 'fr.description', '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(
$queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('fr.deleted', 0),
$queryBuilder->expr()->eq('fr.hidden', 0),
$queryBuilder->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
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);
}
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;
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;
}
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;
}
}

View File

@@ -1,514 +0,0 @@
<?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 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
{
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$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();
if (empty($contentElements)) {
return '';
}
return $this->renderForRecord($contentElements[0]);
}
/**
* Render exactly the given tt_content row (the product-show plugin element).
* Exception-safe: returns '' on any failure.
*
* @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'] ?? [];
$productUid = (int)($settings['product'] ?? 0);
$layout = (int)($settings['layout'] ?? 0);
$debugMode = (bool)($settings['debug'] ?? false);
if (!$productUid) {
$routeParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();
$productParam = $routeParams['tx_vitec_productshow']['product'] ?? null;
if ($productParam) {
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 $debugMode
? json_encode(['error' => 'No product selected or found', 'debug' => ['settings' => $settings]])
: '';
}
$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 $debugMode
? json_encode(['error' => 'Product not found', 'debug' => ['productUid' => $productUid]])
: '';
}
$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);
} catch (\Throwable $e) {
return '';
}
}
/**
* @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' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')),
'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((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),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
];
}
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();
$processedImage = $imageService->applyProcessingInstructions(
$fileReference,
['width' => '1874c', 'height' => '625c']
);
$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) {
continue;
}
}
return $images;
}
/**
* @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;
}
}
/**
* Resolve the uploaded video file (single FAL reference, fieldname=videofile).
*
* @return array<string,mixed>|null
*/
protected function getProductVideoFile(int $productUid): ?array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
$row = $queryBuilder
->select('fr.uid', 'fr.title', 'fr.description', '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(
$queryBuilder->expr()->eq('fr.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.fieldname', $queryBuilder->createNamedParameter('videofile', \Doctrine\DBAL\ParameterType::STRING)),
$queryBuilder->expr()->eq('fr.uid_foreign', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('fr.deleted', 0),
$queryBuilder->expr()->eq('fr.hidden', 0),
$queryBuilder->expr()->eq('f.missing', 0)
)
->orderBy('fr.sorting_foreign', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return null;
}
return [
'uid' => (int)$row['file_uid'],
'name' => (string)($row['name'] ?? ''),
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
'size' => (int)($row['size'] ?? 0),
'extension' => (string)($row['extension'] ?? ''),
'mimeType' => (string)($row['mime_type'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => (string)($row['description'] ?? ''),
];
}
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);
}
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;
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;
}
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;
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory;
@@ -12,106 +14,115 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
/**
* UserFunc to render a single usecase as JSON for headless output.
* UserFunc to render a single solution 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.
* `render()` = page discovery (top-level plugin). `renderForRecord()` =
* one specific tt_content row, reused by ContainerChildrenProcessor and
* ContentElementResolver. Exception-safe.
*/
class UsecaseShowJsonRenderer
class SolutionShowJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_solutionshow') {
return $this->renderForRecord($row);
}
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$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('CType', $queryBuilder->createNamedParameter('vitec_solutionshow', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->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);
// 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);
$solutionUid = (int)($settings['solution'] ?? 0);
$layout = (string)($settings['layout'] ?? 'default');
$debugMode = (bool)($settings['debug'] ?? false);
// Fall back to route / query parameter (uid or slug)
if (!$usecaseUid) {
// Fallback: route / query parameter (numeric uid only — Market has no slug)
if (!$solutionUid) {
$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;
}
$solutionParam = $routeParams['tx_vitec_solutionshow']['solution'] ?? null;
if ($solutionParam && is_numeric($solutionParam)) {
$solutionUid = (int)$solutionParam;
}
}
if (!$usecaseUid) {
if (!$solutionUid) {
return $debugMode
? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]])
? json_encode(['error' => 'No solution selected or found', 'debug' => ['settings' => $settings]])
: '';
}
// Query the usecase
$usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
$solutionQb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
$usecase = $usecaseQb
$solution = $solutionQb
->select('*')
->from('tx_vitec_domain_model_usecase')
->from('tx_vitec_domain_model_solution')
->where(
$usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)),
$usecaseQb->expr()->eq('deleted', 0),
$usecaseQb->expr()->eq('hidden', 0)
$solutionQb->expr()->eq('uid', $solutionQb->createNamedParameter($solutionUid, ParameterType::INTEGER)),
$solutionQb->expr()->eq('deleted', 0),
$solutionQb->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAssociative();
if (!$usecase) {
if (!$solution) {
return $debugMode
? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]])
? json_encode(['error' => 'Solution not found', 'debug' => ['solutionUid' => $solutionUid]])
: '';
}
$response = [
'usecase' => $this->serializeUsecase($usecase),
'solution' => $this->serializeSolution($solution),
'layout' => $layout,
'settings' => [
'layout' => $layout,
@@ -121,7 +132,7 @@ class UsecaseShowJsonRenderer
if ($debugMode) {
$response['debug'] = [
'pageId' => $pageId,
'usecaseUid' => $usecaseUid,
'solutionUid' => $solutionUid,
'layout' => $layout,
'settings' => $settings,
];
@@ -134,39 +145,30 @@ class UsecaseShowJsonRenderer
}
/**
* 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
* @param array<string,mixed> $solution
* @return array<string,mixed>
*/
protected function serializeUsecase(array $usecase): array
protected function serializeSolution(array $solution): array
{
$uid = (int)$usecase['uid'];
$uid = (int)$solution['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'),
'title' => (string)($solution['title'] ?? ''),
'subtitle' => (string)($solution['subtitle'] ?? ''),
'teaser' => (string)($solution['teaser'] ?? ''),
'description' => (string)($solution['description'] ?? ''),
'categories' => $this->getSolutionCategories($uid),
'image' => $this->getSolutionImage($uid),
];
}
/**
* Resolve a single FAL image field (caseimage / logoimage) with srcset.
* Resolve the single image FAL reference for a market (fieldname=image).
*
* @return array<string,mixed>|null
*/
protected function getUsecaseImage(int $usecaseUid, string $fieldName): ?array
protected function getSolutionImage(int $solutionUid): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_file_reference');
@@ -175,9 +177,9 @@ class UsecaseShowJsonRenderer
->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.tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_solution', ParameterType::STRING)),
$queryBuilder->expr()->eq('sfr.fieldname', $queryBuilder->createNamedParameter('image', ParameterType::STRING)),
$queryBuilder->expr()->eq('sfr.uid_foreign', $queryBuilder->createNamedParameter($solutionUid, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('sfr.deleted', 0),
$queryBuilder->expr()->eq('sfr.hidden', 0)
)
@@ -231,10 +233,7 @@ class UsecaseShowJsonRenderer
}
}
/**
* Get categories for a usecase (resolved sys_category records).
*/
protected function getUsecaseCategories(int $usecaseUid): array
protected function getSolutionCategories(int $solutionUid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
@@ -247,12 +246,12 @@ class UsecaseShowJsonRenderer
'sys_category_record_mm',
'mm',
'mm.uid_local = c.uid AND mm.tablenames = ' .
$queryBuilder->createNamedParameter('tx_vitec_domain_model_usecase', ParameterType::STRING) .
$queryBuilder->createNamedParameter('tx_vitec_domain_model_solution', 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('mm.uid_foreign', $queryBuilder->createNamedParameter($solutionUid, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('c.deleted', 0),
$queryBuilder->expr()->eq('c.hidden', 0)
)

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory;
@@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class UsecaseListJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaselist') {
return $this->renderForRecord($row);
}
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$contentElements = $ttContentQb
$contentElements = $queryBuilder
->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)
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaselist', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
@@ -46,6 +69,7 @@ class UsecaseListJsonRenderer
return $this->renderForRecord($contentElements[0]);
}
/**
* @param array<string,mixed> $contentElement
*/

View File

@@ -1,244 +0,0 @@
<?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);
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory;
@@ -20,21 +22,42 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class UsecaseShowJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaseshow') {
return $this->renderForRecord($row);
}
$ttContentQb = GeneralUtility::makeInstance(ConnectionPool::class)
// 2) Page-id via v14 request attribute (TSFE->id is often null in JSON cObj context)
$pageId = 0;
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request !== null) {
$pageInfo = $request->getAttribute('frontend.page.information');
if ($pageInfo !== null) {
$pageId = (int)$pageInfo->getId();
}
}
if ($pageId <= 0) {
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
}
if ($pageId <= 0) {
return '';
}
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$contentElements = $ttContentQb
$contentElements = $queryBuilder
->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)
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('vitec_usecaseshow', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
@@ -46,6 +69,7 @@ class UsecaseShowJsonRenderer
return $this->renderForRecord($contentElements[0]);
}
/**
* @param array<string,mixed> $contentElement
*/

View File

@@ -62,6 +62,11 @@ final class BackendLayoutDataProvider implements DataProviderInterface
'preFooter' => 'Pre-Footer',
];
public function getIdentifier(): string
{
return 'vitec';
}
public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection)
{
foreach (self::LAYOUT_MAP as $value => $info) {

2
packages/vitec/Classes/View/VitecBackendLayoutView.php Executable file → Normal file
View File

@@ -10,7 +10,7 @@ use TYPO3\CMS\Backend\View\BackendLayoutView;
* Overrides BackendLayoutView so the page module always reflects pages.layout
* without the editor having to set backend_layout manually.
*/
final class VitecBackendLayoutView extends BackendLayoutView
final readonly class VitecBackendLayoutView extends BackendLayoutView
{
public function getSelectedCombinedIdentifier(int $pageId): string|false
{

0
packages/vitec/Configuration/FlexForms/Container.xml Executable file → Normal file
View File

View File

@@ -6,60 +6,42 @@
<sheets>
<sDEF>
<ROOT>
<TCEforms>
<sheetTitle>General Settings</sheetTitle>
</TCEforms>
<sheetTitle>General Settings</sheetTitle>
<type>array</type>
<el>
<settings.showFilter>
<TCEforms>
<label>Show Filter</label>
<config>
<type>check</type>
<default>1</default>
</config>
</TCEforms>
<label>Show Filter</label>
<config>
<type>check</type>
<default>1</default>
</config>
</settings.showFilter>
<settings.showSearch>
<TCEforms>
<label>Show Search</label>
<config>
<type>check</type>
<default>1</default>
</config>
</TCEforms>
<label>Show Search</label>
<config>
<type>check</type>
<default>1</default>
</config>
</settings.showSearch>
<settings.itemsPerPage>
<TCEforms>
<label>Items per Page</label>
<config>
<type>number</type>
<default>20</default>
</config>
</TCEforms>
<label>Items per Page</label>
<config>
<type>number</type>
<default>20</default>
</config>
</settings.itemsPerPage>
<settings.defaultCategory>
<TCEforms>
<label>Default Category</label>
<config>
<type>select</type>
<renderType>selectSingle</renderType>
<foreign_table>tx_vitec_domain_model_downloadcategory</foreign_table>
<foreign_table_where>ORDER BY title</foreign_table_where>
<items>
<numIndex index="0">
<label>All Categories</label>
<value>0</value>
</numIndex>
</items>
</config>
</TCEforms>
</settings.defaultCategory>
<settings.debug>
<label>Allow Debug Output</label>
<config>
<type>check</type>
<default>0</default>
</config>
</settings.debug>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>
</T3DataStructure>

View File

@@ -1,29 +0,0 @@
<?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>

44
packages/vitec/Configuration/Icons.php Executable file → Normal file
View File

@@ -4,6 +4,46 @@ declare(strict_types=1);
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
return [
'vitec-plugin-productlist' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productlist.svg',
],
'vitec-plugin-simplecard' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-simplecard.svg',
],
'vitec-plugin-productshow' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productshow.svg',
],
'vitec-plugin-usecaseshow' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaseshow.svg',
],
'vitec-plugin-marketshow' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketshow.svg',
],
'vitec-plugin-solutionshow' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-solutionshow.svg',
],
'vitec-plugin-usecaselist' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaselist.svg',
],
'vitec-plugin-downloadcard' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcard.svg',
],
'vitec-plugin-downloadcardcollection' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcardcollection.svg',
],
'vitec-plugin-datasheets' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg',
],
'vitec-cols-50-50' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-50-50.svg',
@@ -24,6 +64,10 @@ return [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg',
],
'vitec-container' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-container.svg',
],
'vitec-ogimage' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg',

View File

@@ -7,5 +7,4 @@ settings:
dependencies:
- typo3/fluid-styled-content
- friendsoftypo3/headless
- itplusx/headless-container
- nb-headless-content-blocks/headless-content-blocks

View File

@@ -8,91 +8,129 @@ config {
}
}
@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript'
# =============================================================================
# Headless list-plugin JSON renderers
# Headless list-plugin JSON renderers (TYPO3 v14: each plugin is its own CType)
#
# `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).
# For every VITEC plugin the renderer's USER output is exposed as a sub-field
# of the standard content-element envelope (`content.<key>`), inheriting all
# default fields (id, type, colPos, categories, appearance, …) from
# lib.contentElement provided by friendsoftypo3/headless.
# =============================================================================
# --- 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
tt_content {
vitec_productlist < lib.contentElement
vitec_productlist {
fields {
content {
fields {
products = USER
products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
}
}
}
}
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
vitec_productshow < lib.contentElement
vitec_productshow {
fields {
content {
fields {
product = USER
product.userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
}
}
}
}
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
vitec_usecaselist < lib.contentElement
vitec_usecaselist {
fields {
content {
fields {
usecases = USER
usecases.userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
}
}
}
}
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
vitec_usecaseshow < lib.contentElement
vitec_usecaseshow {
fields {
content {
fields {
usecase = USER
usecase.userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
}
}
}
}
vitec_marketshow < lib.contentElement
vitec_marketshow {
fields {
content {
fields {
market = USER
market.userFunc = Evomedien\Vitec\UserFunc\MarketShowJsonRenderer->render
}
}
}
}
vitec_solutionshow < lib.contentElement
vitec_solutionshow {
fields {
content {
fields {
solution = USER
solution.userFunc = Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer->render
}
}
}
}
vitec_downloadcard < lib.contentElement
vitec_downloadcard {
fields {
content {
fields {
downloadcard = USER
downloadcard.userFunc = Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer->render
}
}
}
}
vitec_downloadcardcollection < lib.contentElement
vitec_downloadcardcollection {
fields {
content {
fields {
downloadcardcollection = USER
downloadcardcollection.userFunc = Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer->render
}
}
}
}
vitec_datasheets < lib.contentElement
vitec_datasheets {
fields {
content {
fields {
datasheets = USER
datasheets.userFunc = Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer->render
}
}
}
}
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'

View File

@@ -1,44 +0,0 @@
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'

View File

@@ -1,82 +0,0 @@
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'

0
packages/vitec/Configuration/TCA/Overrides/pages.php Executable file → Normal file
View File

View File

@@ -1,120 +1,53 @@
<?php
declare(strict_types=1);
defined('TYPO3') or die();
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
// Register the Simple Card plugin
$simplecardPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Simplecard',
'Simple Card'
);
(static function (): void {
$registerPluginWithFlexForm = static function (
string $pluginName,
string $pluginTitle,
string $flexFormFile,
?string $iconIdentifier = null
): void {
ExtensionUtility::registerPlugin(
'Vitec',
$pluginName,
$pluginTitle,
$iconIdentifier ?? 'content-plugin',
'plugins',
'',
$flexFormFile
);
};
// Register the "Single Success Story" plugin
$usecaseShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Usecaseshow',
'Single Success Story'
);
$registerPluginWithFlexForm('Productlist', 'Show Products by Category', 'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml', 'vitec-plugin-productlist');
$registerPluginWithFlexForm('Simplecard', 'Simple Card', 'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml', 'vitec-plugin-simplecard');
$registerPluginWithFlexForm('Productshow', 'Show Single Product', 'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml', 'vitec-plugin-productshow');
$registerPluginWithFlexForm('Usecaseshow', 'Single Success Story', 'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml', 'vitec-plugin-usecaseshow');
$registerPluginWithFlexForm('Marketshow', 'Show Single Market', 'FILE:EXT:vitec/Configuration/FlexForms/Market.xml', 'vitec-plugin-marketshow');
$registerPluginWithFlexForm('Solutionshow', 'Show Single Solution', 'FILE:EXT:vitec/Configuration/FlexForms/Solution.xml', 'vitec-plugin-solutionshow');
$registerPluginWithFlexForm('Usecaselist', 'Shows List of all Success Stories', 'FILE:EXT:vitec/Configuration/FlexForms/Usecaselist.xml', 'vitec-plugin-usecaselist');
$registerPluginWithFlexForm('Downloadcard', 'Download Card', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcard.xml', 'vitec-plugin-downloadcard');
$registerPluginWithFlexForm('Downloadcardcollection', 'Download Card Collection', 'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml', 'vitec-plugin-downloadcardcollection');
$registerPluginWithFlexForm('Datasheets', 'Datasheets', 'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml');
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$usecaseShowPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$usecaseShowPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml'
);
// Change frame_class to allow multiple selections.
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['maxitems'] = 999;
// Register the "Show Single Market" plugin
$marketShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Marketshow',
'Show Single Market'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$marketShowPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$marketShowPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Market.xml'
);
// Register the "Show Single Solution" plugin
$solutionShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Solutionshow',
'Show Single Solution'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$solutionShowPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$solutionShowPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Solution.xml'
);
// Register the "Show Products by Category" plugin
$productListPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Productlist',
'Show Products by Category'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$productListPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$productListPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml'
);
// Register the "Show Single Product" plugin
$productShowPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Productshow',
'Show Single Product'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$productShowPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$productShowPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml'
);
// Register the "Shows List of all Success Stories" plugin
$usecaseListPluginSignature = \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Usecaselist',
'Shows List of all Success Stories'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$usecaseListPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$usecaseListPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Usecaselist.xml'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$simplecardPluginSignature] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
$simplecardPluginSignature,
'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['vitec_datasheets'] = 'recursive,select_key,pages';
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_datasheets'] = 'pi_flexform';
ExtensionManagementUtility::addPiFlexFormValue(
'vitec_datasheets',
'FILE:EXT:vitec/Configuration/FlexForms/Datasheets.xml'
);
// Change frame_class to allow multiple selections
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['maxitems'] = 999;
// Add custom frame_class options for Vitec
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'] = array_merge(
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'],
[
['label' => 'Vitec: Full Width', 'value' => 'vitec-full-width'],
['label' => 'Vitec: Centered Container', 'value' => 'vitec-centered'],
['label' => 'Vitec: Card Style', 'value' => 'vitec-card'],
['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'],
['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'],
]
);
// Add custom frame_class options for Vitec.
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'] = array_merge(
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['items'],
[
['label' => 'Vitec: Full Width', 'value' => 'vitec-full-width'],
['label' => 'Vitec: Centered Container', 'value' => 'vitec-centered'],
['label' => 'Vitec: Card Style', 'value' => 'vitec-card'],
['label' => 'Vitec: Dark Background', 'value' => 'vitec-dark'],
['label' => 'Vitec: Highlight Box', 'value' => 'vitec-highlight'],
]
);
})();

View File

@@ -47,7 +47,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
--palette--;;hidden,
--palette--;;access';
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_25_25_25_25 {
iconIdentifier = vitec-cols-25-25-25-25
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_25_25_25_25.title
@@ -56,5 +56,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_25_25_25_25
CType = vitec_cols_25_25_25_25
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -46,7 +46,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
--palette--;;hidden,
--palette--;;access';
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_33_33 {
iconIdentifier = vitec-cols-33-33-33
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_33_33.title
@@ -55,5 +55,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_33_33 {
CType = vitec_cols_33_33_33
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
--palette--;;hidden,
--palette--;;access';
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_66 {
iconIdentifier = vitec-cols-33-66
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_33_66.title
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_33_66 {
CType = vitec_cols_33_66
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
--palette--;;hidden,
--palette--;;access';
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_50_50 {
iconIdentifier = vitec-cols-50-50
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_50_50.title
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_50_50 {
CType = vitec_cols_50_50
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -45,7 +45,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
--palette--;;hidden,
--palette--;;access';
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_66_33 {
iconIdentifier = vitec-cols-66-33
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:cols_66_33.title
@@ -54,5 +54,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_cols_66_33 {
CType = vitec_cols_66_33
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -56,7 +56,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
'vitec_container'
);
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
iconIdentifier = vitec-container
title = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.title
@@ -65,5 +65,5 @@ mod.wizards.newContentElement.wizardItems.vitec.elements.vitec_container {
CType = vitec_container
}
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -1,64 +0,0 @@
<?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);
})();

View File

@@ -28,10 +28,10 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
]);
// Register VITEC wizard group header
ExtensionManagementUtility::addPageTSConfig(<<<TSCONFIG
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '') . "\n" . <<<'TSCONFIG'
mod.wizards.newContentElement.wizardItems.vitec {
header = LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header
show = *
}
TSCONFIG);
TSCONFIG;
})();

View File

@@ -192,27 +192,27 @@ return [
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'
],
\TYPO3\CMS\Core\Resource\File::FILETYPE_TEXT => [
\TYPO3\CMS\Core\Resource\FileType::TEXT->value => [
'showitem' => '
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'
],
\TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [
\TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [
'showitem' => '
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'
],
\TYPO3\CMS\Core\Resource\File::FILETYPE_AUDIO => [
\TYPO3\CMS\Core\Resource\FileType::AUDIO->value => [
'showitem' => '
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'
],
\TYPO3\CMS\Core\Resource\File::FILETYPE_VIDEO => [
\TYPO3\CMS\Core\Resource\FileType::VIDEO->value => [
'showitem' => '
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'
],
\TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [
\TYPO3\CMS\Core\Resource\FileType::APPLICATION->value => [
'showitem' => '
--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
--palette--;;filePalette'

View File

View File

@@ -1,541 +0,0 @@
<?php
return [
'ctrl' => [
'title' => 'VITEC Product',
'label' => 'title',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'cruser_id' => 'cruser_id',
'versioningWS' => true,
'languageField' => 'sys_language_uid',
'transOrigPointerField' => 'l10n_parent',
'transOrigDiffSourceField' => 'l10n_diffsource',
'delete' => 'deleted',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'searchFields' => 'title,slug',
'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_product.gif',
'security' => [
'ignorePageTypeRestriction' => true,
],
],
'types' => [
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, applications, highlights, contentelement, contentelementcta, downloads,
--div--;SEO, seotitle, urltitle, seometa, keywords, structureddata,
--div--;Images and Videos, productimage, ogimage, video,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
--div--;Visibility, hideonapp, hideonwebsite, hideondatasheets, hideonproducts, shortcut, shortcutpid,
--div--;Misc, legacy, supportproduct, subproduct, relatedprodukt,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime'],
],
'columns' => [
'sys_language_uid' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [
'type' => 'language',
],
],
'l10n_parent' => [
'displayCond' => 'FIELD:sys_language_uid:>:0',
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'default' => 0,
'items' => [
['', 0],
],
'foreign_table' => 'tx_vitec_domain_model_product',
'foreign_table_where' => 'AND {#tx_vitec_domain_model_product}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_product}.{#sys_language_uid} IN (-1,0)',
],
],
'l10n_diffsource' => [
'config' => [
'type' => 'passthrough',
],
],
'hidden' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => true
]
],
],
],
'starttime' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'eval' => 'datetime,int',
'default' => 0,
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'endtime' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'eval' => 'datetime,int',
'default' => 0,
'range' => [
'upper' => mktime(0, 0, 0, 1, 1, 2038)
],
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'categories' => [
'config' => [
'type' => 'category'
]
],
'title' => [
'exclude' => false,
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title.description',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'required' => true,
'default' => ''
],
],
'slug' => [
'exclude' => false,
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug.description',
'config' => [
'type' => 'slug',
'size' => 50,
'generatorOptions' => [
'fields' => ['title'], // TODO: adjust this field to the one you want to use
'fieldSeparator' => '-',
'replacements' => [
'/' => '',
],
],
'fallbackCharacter' => '-',
'eval' => 'uniqueInPid',
],
],
'seotitle' => [
'exclude' => false,
'label' => 'SEO-Title',
'description' => 'SEO specific title, used for sharing options',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'urltitle' => [
'exclude' => false,
'label' => 'URL-Title',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.urltitle.description',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'video' => [
'exclude' => false,
'label' => 'Product Video - just the Youtube Clip ID',
'description' => 'Product Video - just the Youtube Clip ID',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'downloads' => [
'exclude' => true,
'label' => 'Downloads',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_vitec_domain_model_download',
'MM' => 'tx_vitec_product_download_mm', // Define the MM table for the relation
'size' => 10,
'maxitems' => 9999,
'minitems' => 0,
'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering
],
],
'structureddata' => [
'exclude' => true,
'label' => 'Structured Data in JSON Format',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'keywords' => [
'exclude' => true,
'label' => 'Keywords',
'description' => 'Keywords for SEO, separated by commas',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'seometa' => [
'exclude' => true,
'label' => 'SEO Meta Description',
'description' => 'SEO specific meta description, used for sharing options',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'teaser' => [
'exclude' => true,
'label' => 'Short Teaser (optional)',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'subtitle' => [
'exclude' => true,
'label' => 'Subtitle',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'hideonapp' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on App',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideonwebsite' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Website',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideonproducts' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Product Section',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideondatasheets' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Datasheets Page',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'applications' => [
'exclude' => true,
'label' => 'Applications',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'description' => [
'exclude' => true,
'label' => 'Description',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'highlights' => [
'exclude' => true,
'label' => 'Highlights',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'shortcut' => [
'exclude' => true,
'label' => 'Does this Product have a custom Page',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'shortcutpid' => [
'exclude' => false,
'label' => 'Page ID',
'description' => 'PID of the custom page',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'legacy' => [
'exclude' => true,
'label' => 'Is this a legacy produc?',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'supportproduct' => [
'exclude' => true,
'label' => 'Is this a support product?',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'subproduct' => [
'exclude' => true,
'label' => 'Product is a sub-products',
'description' => 'This Product will not be shown as a main product',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'relatedprodukt' => [
'exclude' => true,
'label' => 'Related Products',
'description' => 'Select related products for this product',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_vitec_domain_model_product', // Reference the same table
'MM' => 'tx_vitec_product_related_mm', // Define the MM table for the relation
'size' => 10,
'maxitems' => 9999,
'minitems' => 0,
'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering
],
],
'productimage' => [
'exclude' => true,
'label' => 'Product Image(s)',
'config' => [
'type' => 'inline',
'foreign_table' => 'sys_file_reference',
'foreign_field' => 'uid_foreign',
'foreign_sortby' => 'sorting_foreign',
'foreign_table_field' => 'tablenames',
'foreign_match_fields' => [
'fieldname' => 'productimage',
],
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'showSynchronizationLink' => true,
'showPossibleLocalizationRecords' => true,
'showAllLocalizationLink' => true,
],
'behaviour' => [
'allowLanguageSynchronization' => true,
],
'filter' => [
[
'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
'parameters' => [
'allowedFileExtensions' => 'jpg,jpeg,png,gif',
],
],
],
'maxitems' => 10,
'minitems' => 0,
],
],
'ogimage' => [
'exclude' => true,
'label' => 'Open Graph Image',
'config' => [
'type' => 'inline',
'foreign_table' => 'sys_file_reference',
'foreign_field' => 'uid_foreign',
'foreign_sortby' => 'sorting_foreign',
'foreign_table_field' => 'tablenames',
'foreign_match_fields' => [
'fieldname' => 'ogimage',
],
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'showSynchronizationLink' => true,
'showPossibleLocalizationRecords' => true,
'showAllLocalizationLink' => true,
],
'behaviour' => [
'allowLanguageSynchronization' => true,
],
'filter' => [
[
'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
'parameters' => [
'allowedFileExtensions' => 'jpg,jpeg,png,gif,webp',
],
],
],
'maxitems' => 1, // Allow only one image
'minitems' => 0,
],
],
'contentelement' => [
'exclude' => true,
'label' => 'Select Content Element for Key Features Section',
'config' => [
'type' => 'input',
'renderType' => 'inputLink',
'softref' => 'typolink',
'eval' => 'trim',
'fieldControl' => [
'linkPopup' => [
'options' => [
'title' => 'Select Content Element',
'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types
'blindLinkFields' => 'class,params', // Disable unnecessary fields
],
],
],
],
],
'contentelementcta' => [
'exclude' => true,
'label' => 'Select Content Element for CTA Section',
'config' => [
'type' => 'input',
'renderType' => 'inputLink',
'softref' => 'typolink',
'eval' => 'trim',
'fieldControl' => [
'linkPopup' => [
'options' => [
'title' => 'Select Content Element',
'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types
'blindLinkFields' => 'class,params', // Disable unnecessary fields
],
],
],
],
],
/* ----------------------------------------------------- */
],
];

View File

@@ -1,576 +0,0 @@
<?php
return [
'ctrl' => [
'title' => 'VITEC Product',
'label' => 'title',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'cruser_id' => 'cruser_id',
'versioningWS' => true,
'languageField' => 'sys_language_uid',
'transOrigPointerField' => 'l10n_parent',
'transOrigDiffSourceField' => 'l10n_diffsource',
'delete' => 'deleted',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'searchFields' => 'title,slug',
'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_product.gif',
'security' => [
'ignorePageTypeRestriction' => true,
],
],
'types' => [
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, applications, highlights, contentelement, contentelementcta, downloads,
--div--;SEO, seotitle, urltitle, seometa, keywords, structureddata,
--div--;Images and Videos, productimage, ogimage, video, videofile,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
--div--;Visibility, hideonapp, hideonwebsite, hideondatasheets, hideonproducts, shortcut, shortcutpid,
--div--;Misc, legacy, supportproduct, subproduct, relatedprodukt,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime'],
],
'columns' => [
'sys_language_uid' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
'config' => [
'type' => 'language',
],
],
'l10n_parent' => [
'displayCond' => 'FIELD:sys_language_uid:>:0',
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'default' => 0,
'items' => [
['', 0],
],
'foreign_table' => 'tx_vitec_domain_model_product',
'foreign_table_where' => 'AND {#tx_vitec_domain_model_product}.{#pid}=###CURRENT_PID### AND {#tx_vitec_domain_model_product}.{#sys_language_uid} IN (-1,0)',
],
],
'l10n_diffsource' => [
'config' => [
'type' => 'passthrough',
],
],
'hidden' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => true
]
],
],
],
'starttime' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'eval' => 'datetime,int',
'default' => 0,
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'endtime' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'eval' => 'datetime,int',
'default' => 0,
'range' => [
'upper' => mktime(0, 0, 0, 1, 1, 2038)
],
'behaviour' => [
'allowLanguageSynchronization' => true
]
],
],
'categories' => [
'config' => [
'type' => 'category'
]
],
'title' => [
'exclude' => false,
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.title.description',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'required' => true,
'default' => ''
],
],
'slug' => [
'exclude' => false,
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.slug.description',
'config' => [
'type' => 'slug',
'size' => 50,
'generatorOptions' => [
'fields' => ['title'], // TODO: adjust this field to the one you want to use
'fieldSeparator' => '-',
'replacements' => [
'/' => '',
],
],
'fallbackCharacter' => '-',
'eval' => 'uniqueInPid',
],
],
'seotitle' => [
'exclude' => false,
'label' => 'SEO-Title',
'description' => 'SEO specific title, used for sharing options',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'urltitle' => [
'exclude' => false,
'label' => 'URL-Title',
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_domain_model_product.urltitle.description',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'video' => [
'exclude' => false,
'label' => 'Product Video - just the Youtube Clip ID',
'description' => 'Product Video - just the Youtube Clip ID',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'downloads' => [
'exclude' => true,
'label' => 'Downloads',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_vitec_domain_model_download',
'MM' => 'tx_vitec_product_download_mm', // Define the MM table for the relation
'size' => 10,
'maxitems' => 9999,
'minitems' => 0,
'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering
],
],
'structureddata' => [
'exclude' => true,
'label' => 'Structured Data in JSON Format',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'keywords' => [
'exclude' => true,
'label' => 'Keywords',
'description' => 'Keywords for SEO, separated by commas',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'seometa' => [
'exclude' => true,
'label' => 'SEO Meta Description',
'description' => 'SEO specific meta description, used for sharing options',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 15,
'eval' => 'trim'
]
],
'teaser' => [
'exclude' => true,
'label' => 'Short Teaser (optional)',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'subtitle' => [
'exclude' => true,
'label' => 'Subtitle',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'hideonapp' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on App',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideonwebsite' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Website',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideonproducts' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Product Section',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'hideondatasheets' => [
'exclude' => true,
'label' => 'If checked - Product will not be shown on Datasheets Page',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'applications' => [
'exclude' => true,
'label' => 'Applications',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'description' => [
'exclude' => true,
'label' => 'Description',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'highlights' => [
'exclude' => true,
'label' => 'Highlights',
'config' => [
'type' => 'text',
'enableRichtext' => 'true',
'eval' => 'trim',
'default' => ''
],
'defaultExtras' => 'richtext:rte_transform[mode=ts_css]'
],
'shortcut' => [
'exclude' => true,
'label' => 'Does this Product have a custom Page',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'shortcutpid' => [
'exclude' => false,
'label' => 'Page ID',
'description' => 'PID of the custom page',
'config' => [
'type' => 'input',
'size' => 30,
'eval' => 'trim',
'default' => ''
],
],
'legacy' => [
'exclude' => true,
'label' => 'Is this a legacy produc?',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'supportproduct' => [
'exclude' => true,
'label' => 'Is this a support product?',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'subproduct' => [
'exclude' => true,
'label' => 'Product is a sub-products',
'description' => 'This Product will not be shown as a main product',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
0 => '',
1 => '',
'invertStateDisplay' => false
]
],
],
],
'relatedprodukt' => [
'exclude' => true,
'label' => 'Related Products',
'description' => 'Select related products for this product',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_vitec_domain_model_product', // Reference the same table
'MM' => 'tx_vitec_product_related_mm', // Define the MM table for the relation
'size' => 10,
'maxitems' => 9999,
'minitems' => 0,
'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering
],
],
'productimage' => [
'exclude' => true,
'label' => 'Product Image(s)',
'config' => [
'type' => 'inline',
'foreign_table' => 'sys_file_reference',
'foreign_field' => 'uid_foreign',
'foreign_sortby' => 'sorting_foreign',
'foreign_table_field' => 'tablenames',
'foreign_match_fields' => [
'fieldname' => 'productimage',
],
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'showSynchronizationLink' => true,
'showPossibleLocalizationRecords' => true,
'showAllLocalizationLink' => true,
],
'behaviour' => [
'allowLanguageSynchronization' => true,
],
'filter' => [
[
'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
'parameters' => [
'allowedFileExtensions' => 'jpg,jpeg,png,gif',
],
],
],
'maxitems' => 10,
'minitems' => 0,
],
],
'ogimage' => [
'exclude' => true,
'label' => 'Open Graph Image',
'config' => [
'type' => 'inline',
'foreign_table' => 'sys_file_reference',
'foreign_field' => 'uid_foreign',
'foreign_sortby' => 'sorting_foreign',
'foreign_table_field' => 'tablenames',
'foreign_match_fields' => [
'fieldname' => 'ogimage',
],
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'showSynchronizationLink' => true,
'showPossibleLocalizationRecords' => true,
'showAllLocalizationLink' => true,
],
'behaviour' => [
'allowLanguageSynchronization' => true,
],
'filter' => [
[
'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
'parameters' => [
'allowedFileExtensions' => 'jpg,jpeg,png,gif,webp',
],
],
],
'maxitems' => 1, // Allow only one image
'minitems' => 0,
],
],
'videofile' => [
'exclude' => true,
'label' => 'Video File (Upload or Local)',
'config' => [
'type' => 'inline',
'foreign_table' => 'sys_file_reference',
'foreign_field' => 'uid_foreign',
'foreign_sortby' => 'sorting_foreign',
'foreign_table_field' => 'tablenames',
'foreign_match_fields' => [
'fieldname' => 'videofile',
],
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'showSynchronizationLink' => true,
'showPossibleLocalizationRecords' => true,
'showAllLocalizationLink' => true,
],
'behaviour' => [
'allowLanguageSynchronization' => true,
],
'filter' => [
[
'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
'parameters' => [
'allowedFileExtensions' => 'mp4,webm,ogv,mov,m4v',
],
],
],
'maxitems' => 1,
'minitems' => 0,
],
],
'contentelement' => [
'exclude' => true,
'label' => 'Select Content Element for Key Features Section',
'config' => [
'type' => 'input',
'renderType' => 'inputLink',
'softref' => 'typolink',
'eval' => 'trim',
'fieldControl' => [
'linkPopup' => [
'options' => [
'title' => 'Select Content Element',
'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types
'blindLinkFields' => 'class,params', // Disable unnecessary fields
],
],
],
],
],
'contentelementcta' => [
'exclude' => true,
'label' => 'Select Content Element for CTA Section',
'config' => [
'type' => 'input',
'renderType' => 'inputLink',
'softref' => 'typolink',
'eval' => 'trim',
'fieldControl' => [
'linkPopup' => [
'options' => [
'title' => 'Select Content Element',
'blindLinkOptions' => 'mail,folder,url', // Disable unnecessary link types
'blindLinkFields' => 'class,params', // Disable unnecessary fields
],
],
],
],
],
/* ----------------------------------------------------- */
],
];

View File

@@ -1,89 +0,0 @@
# =============================================================================
# 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

View File

@@ -13,8 +13,7 @@ mod {
title = List of VITEC Products
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_productlist.description
tt_content_defValues {
CType = list
list_type = vitec_productlist
CType = vitec_productlist
}
}
productshow {
@@ -22,8 +21,7 @@ mod {
title = Show single VITEC Product
description = Either show a single product coming from a list, OR select a specific product in flexform
tt_content_defValues {
CType = list
list_type = vitec_productshow
CType = vitec_productshow
}
}
usecaseshow {
@@ -31,8 +29,7 @@ mod {
title = Show single VITEC Success Story
description = Shows a selected Success Story in a Card with different layouts
tt_content_defValues {
CType = list
list_type = vitec_usecaseshow
CType = vitec_usecaseshow
}
}
marketshow {
@@ -40,8 +37,7 @@ mod {
title = Show single VITEC Market
description = Shows a selected Market in a Card with different layouts
tt_content_defValues {
CType = list
list_type = vitec_marketshow
CType = vitec_marketshow
}
}
solutionshow {
@@ -49,8 +45,7 @@ mod {
title = Show single VITEC Solution
description = Shows a selected Solution in a Card with different layouts
tt_content_defValues {
CType = list
list_type = vitec_solutionshow
CType = vitec_solutionshow
}
}
usecaselist {
@@ -58,8 +53,7 @@ mod {
title = Show all VITEC Success Stories
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
tt_content_defValues {
CType = list
list_type = vitec_usecaselist
CType = vitec_usecaselist
}
}
downloadcard {
@@ -67,8 +61,7 @@ mod {
title = Downloadcard for single Download
description = LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:tx_vitec_usecaselist.description
tt_content_defValues {
CType = list
list_type = vitec_downloadcard
CType = vitec_downloadcard
}
}
downloadcardcollection {
@@ -76,8 +69,7 @@ mod {
title = Downloadcard Collection
description = Multiple Downloads in one Card
tt_content_defValues {
CType = list
list_type = vitec_downloadcardcollection
CType = vitec_downloadcardcollection
}
}
datasheets {
@@ -85,8 +77,7 @@ mod {
title = VITEC Datasheets
description = Interactive datasheets with filtering and download functionality
tt_content_defValues {
CType = list
list_type = vitec_datasheets
CType = vitec_datasheets
}
}
simplecard {
@@ -94,8 +85,7 @@ mod {
title = Simple Card
description = Simple Card (Image, Title, Text, Link)
tt_content_defValues {
CType = list
list_type = vitec_simplecard
CType = vitec_simplecard
}
}

View File

@@ -0,0 +1,7 @@
<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.4" fill="none" stroke="#000" stroke-width="1.2"/>
<rect x="3" y="4" width="10" height="3.6" rx="0.4" fill="#000" opacity="0.55"/>
<rect x="3" y="8.3" width="7.5" height="1.2" rx="0.2" fill="#000" opacity="0.45"/>
<rect x="3" y="10" width="9" height="1" rx="0.2" fill="#000" opacity="0.3"/>
<rect x="3" y="11.4" width="4.2" height="1" rx="0.2" fill="#000" opacity="0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,186 @@
name: vitec/card
group: vitec
prefixFields: true
prefixType: vendor
fields:
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_content
type: Tab
label: Content
- identifier: eyebrow
type: Text
max: 50
- identifier: header
useExistingField: true
required: true
- identifier: subheader
useExistingField: true
- identifier: bodytext
useExistingField: true
enableRichtext: true
- identifier: image
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
- identifier: icon
type: File
minitems: 0
maxitems: 1
allowed: svg,png
- identifier: primary_cta
type: Link
allowedTypes:
- page
- url
- file
- email
- identifier: primary_cta_label
type: Text
max: 30
default: 'Learn more'
- identifier: secondary_cta
type: Link
allowedTypes:
- page
- url
- file
- email
- identifier: secondary_cta_label
type: Text
max: 30
- identifier: card_link
type: Link
allowedTypes:
- page
- url
- file
- email
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_layout
type: Tab
label: Layout
- identifier: layout_variant
type: Select
renderType: selectSingle
default: vertical
items:
- label: Vertical (image top, content below)
value: vertical
- label: Horizontal (image left, content right)
value: horizontal
- label: Horizontal Reverse (content left, image right)
value: horizontal_reverse
- label: Image Overlay (text over image)
value: image_overlay
- label: Text Only (no image)
value: text_only
- label: Icon Card (icon focused, text below)
value: icon_card
- label: Feature (large highlighted card)
value: feature
- label: Compact (small card, tight spacing)
value: compact
- identifier: background_variant
type: Select
renderType: selectSingle
default: none
items:
- label: None
value: none
- label: Orange
value: orange
- label: Blue
value: blue
- label: Graphite
value: graphite
- label: Midnight
value: midnight
- label: Light (neutral)
value: light
- label: Dark (neutral)
value: dark
- identifier: image_aspect_ratio
type: Select
renderType: selectSingle
default: auto
items:
- label: Auto (original)
value: auto
- label: Square (1:1)
value: '1_1'
- label: Landscape (4:3)
value: '4_3'
- label: Widescreen (16:9)
value: '16_9'
- label: Portrait (3:4)
value: '3_4'
- identifier: text_alignment
type: Select
renderType: selectSingle
default: left
items:
- label: Left
value: left
- label: Center
value: center
- label: Right
value: right
- identifier: border_style
type: Select
renderType: selectSingle
default: none
items:
- label: None
value: none
- label: Soft (subtle line)
value: soft
- label: Hard (thick line)
value: hard
- label: Dashed
value: dashed
- identifier: shadow
type: Select
renderType: selectSingle
default: none
items:
- label: None
value: none
- label: Soft
value: soft
- label: Medium
value: medium
- label: Strong
value: strong
# ──────────────────────────────────────────────────────────────────────
- identifier: tab_advanced
type: Tab
label: Advanced
- identifier: cssClass
type: Text
max: 100
- identifier: debug
type: Checkbox
default: 0

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<trans-unit id="title">
<source>VITEC · Card</source>
</trans-unit>
<trans-unit id="description">
<source>Multi-purpose card with image / icon, body, two CTAs, layout/background/aspect-ratio/alignment/border/shadow options.</source>
</trans-unit>
<trans-unit id="tab_content.label">
<source>Content</source>
</trans-unit>
<trans-unit id="tab_layout.label">
<source>Layout</source>
</trans-unit>
<trans-unit id="tab_advanced.label">
<source>Advanced</source>
</trans-unit>
<trans-unit id="eyebrow.label">
<source>Eyebrow (small pre-title)</source>
</trans-unit>
<trans-unit id="image.label">
<source>Image</source>
</trans-unit>
<trans-unit id="image.description">
<source>Primary card image. Optional — leave empty for text-only / icon-only layouts.</source>
</trans-unit>
<trans-unit id="icon.label">
<source>Icon (SVG or PNG)</source>
</trans-unit>
<trans-unit id="icon.description">
<source>Small icon used as fallback when no image is set, or as a visual indicator in icon-focused layouts.</source>
</trans-unit>
<trans-unit id="primary_cta.label">
<source>Primary CTA Link</source>
</trans-unit>
<trans-unit id="primary_cta_label.label">
<source>Primary CTA Button Text</source>
</trans-unit>
<trans-unit id="secondary_cta.label">
<source>Secondary CTA Link (optional)</source>
</trans-unit>
<trans-unit id="secondary_cta_label.label">
<source>Secondary CTA Button Text (optional)</source>
</trans-unit>
<trans-unit id="card_link.label">
<source>Whole-card link (optional)</source>
</trans-unit>
<trans-unit id="card_link.description">
<source>If set, the whole card becomes clickable. Overrides individual CTAs visually in the frontend.</source>
</trans-unit>
<!-- ============================================================== -->
<!-- layout_variant -->
<!-- ============================================================== -->
<trans-unit id="layout_variant.label">
<source>Layout Variant</source>
</trans-unit>
<trans-unit id="layout_variant.items.vertical.label">
<source>Vertical (image top, content below)</source>
</trans-unit>
<trans-unit id="layout_variant.items.horizontal.label">
<source>Horizontal (image left, content right)</source>
</trans-unit>
<trans-unit id="layout_variant.items.horizontal_reverse.label">
<source>Horizontal Reverse (content left, image right)</source>
</trans-unit>
<trans-unit id="layout_variant.items.image_overlay.label">
<source>Image Overlay (text over image)</source>
</trans-unit>
<trans-unit id="layout_variant.items.text_only.label">
<source>Text Only (no image)</source>
</trans-unit>
<trans-unit id="layout_variant.items.icon_card.label">
<source>Icon Card (icon focused, text below)</source>
</trans-unit>
<trans-unit id="layout_variant.items.feature.label">
<source>Feature (large highlighted card)</source>
</trans-unit>
<trans-unit id="layout_variant.items.compact.label">
<source>Compact (small card, tight spacing)</source>
</trans-unit>
<!-- ============================================================== -->
<!-- background_variant -->
<!-- ============================================================== -->
<trans-unit id="background_variant.label">
<source>Background Variant</source>
</trans-unit>
<trans-unit id="background_variant.items.none.label">
<source>None</source>
</trans-unit>
<trans-unit id="background_variant.items.orange.label">
<source>Orange</source>
</trans-unit>
<trans-unit id="background_variant.items.blue.label">
<source>Blue</source>
</trans-unit>
<trans-unit id="background_variant.items.graphite.label">
<source>Graphite</source>
</trans-unit>
<trans-unit id="background_variant.items.midnight.label">
<source>Midnight</source>
</trans-unit>
<trans-unit id="background_variant.items.light.label">
<source>Light (neutral)</source>
</trans-unit>
<trans-unit id="background_variant.items.dark.label">
<source>Dark (neutral)</source>
</trans-unit>
<!-- ============================================================== -->
<!-- image_aspect_ratio -->
<!-- ============================================================== -->
<trans-unit id="image_aspect_ratio.label">
<source>Image Aspect Ratio</source>
</trans-unit>
<trans-unit id="image_aspect_ratio.items.auto.label">
<source>Auto (original)</source>
</trans-unit>
<trans-unit id="image_aspect_ratio.items.1_1.label">
<source>Square (1:1)</source>
</trans-unit>
<trans-unit id="image_aspect_ratio.items.4_3.label">
<source>Landscape (4:3)</source>
</trans-unit>
<trans-unit id="image_aspect_ratio.items.16_9.label">
<source>Widescreen (16:9)</source>
</trans-unit>
<trans-unit id="image_aspect_ratio.items.3_4.label">
<source>Portrait (3:4)</source>
</trans-unit>
<!-- ============================================================== -->
<!-- text_alignment -->
<!-- ============================================================== -->
<trans-unit id="text_alignment.label">
<source>Text Alignment</source>
</trans-unit>
<trans-unit id="text_alignment.items.left.label">
<source>Left</source>
</trans-unit>
<trans-unit id="text_alignment.items.center.label">
<source>Center</source>
</trans-unit>
<trans-unit id="text_alignment.items.right.label">
<source>Right</source>
</trans-unit>
<!-- ============================================================== -->
<!-- border_style -->
<!-- ============================================================== -->
<trans-unit id="border_style.label">
<source>Border Style</source>
</trans-unit>
<trans-unit id="border_style.items.none.label">
<source>None</source>
</trans-unit>
<trans-unit id="border_style.items.soft.label">
<source>Soft (subtle line)</source>
</trans-unit>
<trans-unit id="border_style.items.hard.label">
<source>Hard (thick line)</source>
</trans-unit>
<trans-unit id="border_style.items.dashed.label">
<source>Dashed</source>
</trans-unit>
<!-- ============================================================== -->
<!-- shadow -->
<!-- ============================================================== -->
<trans-unit id="shadow.label">
<source>Shadow</source>
</trans-unit>
<trans-unit id="shadow.items.none.label">
<source>None</source>
</trans-unit>
<trans-unit id="shadow.items.soft.label">
<source>Soft</source>
</trans-unit>
<trans-unit id="shadow.items.medium.label">
<source>Medium</source>
</trans-unit>
<trans-unit id="shadow.items.strong.label">
<source>Strong</source>
</trans-unit>
<trans-unit id="cssClass.label">
<source>Additional CSS Class</source>
</trans-unit>
<trans-unit id="cssClass.description">
<source>Custom CSS class rendered on the card root element in the frontend.</source>
</trans-unit>
<trans-unit id="debug.label">
<source>Allow Debug Output</source>
</trans-unit>
<trans-unit id="debug.description">
<source>When enabled, extra debug fields may be included in the JSON output for this element.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,150 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview vitec-preview--{data.background_variant}">
<f:comment>Thumbnail: image > icon > placeholder</f:comment>
<f:if condition="{data.image.0}">
<f:then>
<f:image image="{data.image.0}"
class="vitec-preview__thumb"
width="120c"
height="80c"
alt="Card image preview"/>
</f:then>
<f:else>
<f:if condition="{data.icon.0}">
<f:then>
<f:image image="{data.icon.0}"
class="vitec-preview__thumb"
width="120c"
height="80"
alt="Card icon preview"/>
</f:then>
<f:else>
<div class="vitec-preview__thumb-placeholder">
<f:switch expression="{data.layout_variant}">
<f:case value="text_only">📝</f:case>
<f:case value="icon_card"></f:case>
<f:case value="feature"></f:case>
<f:case value="compact"></f:case>
<f:defaultCase></f:defaultCase>
</f:switch>
</div>
</f:else>
</f:if>
</f:else>
</f:if>
<div class="vitec-preview__body">
<div class="vitec-preview__label">VITEC · Card</div>
<f:if condition="{data.eyebrow}">
<div class="vitec-preview__eyebrow">{data.eyebrow}</div>
</f:if>
<h3 class="vitec-preview__headline">
<f:if condition="{data.header}">
<f:then>{data.header}</f:then>
<f:else>
<em style="color:#c00;">⚠ Headline fehlt</em>
</f:else>
</f:if>
</h3>
<f:if condition="{data.subheader}">
<div class="vitec-preview__subline">{data.subheader}</div>
</f:if>
<f:if condition="{data.bodytext}">
<div class="vitec-preview__body-text">
<f:format.stripTags>{data.bodytext}</f:format.stripTags>
</div>
</f:if>
<div class="vitec-preview__ctas">
<f:if condition="{data.primary_cta_label}">
<span class="vitec-preview__cta-btn">{data.primary_cta_label} →</span>
</f:if>
<f:if condition="{data.secondary_cta_label}">
<span class="vitec-preview__cta-btn vitec-preview__cta-btn--secondary">
{data.secondary_cta_label}
</span>
</f:if>
</div>
</div>
<f:comment>Settings sidebar with badges</f:comment>
<div class="vitec-preview__settings">
<span class="vitec-badge">
<f:switch expression="{data.layout_variant}">
<f:case value="vertical">▤ Vertical</f:case>
<f:case value="horizontal">▥ Horizontal</f:case>
<f:case value="horizontal_reverse">▤ Horizontal-Rev</f:case>
<f:case value="image_overlay">▣ Image Overlay</f:case>
<f:case value="text_only">📝 Text Only</f:case>
<f:case value="icon_card">✦ Icon Card</f:case>
<f:case value="feature">⭐ Feature</f:case>
<f:case value="compact">▫ Compact</f:case>
<f:defaultCase>{data.layout_variant}</f:defaultCase>
</f:switch>
</span>
<span class="vitec-badge">
<span class="vitec-badge__dot vitec-badge__dot--{data.background_variant}"></span>
<f:switch expression="{data.background_variant}">
<f:case value="none">No BG</f:case>
<f:case value="orange">Orange</f:case>
<f:case value="blue">Blue</f:case>
<f:case value="graphite">Graphite</f:case>
<f:case value="midnight">Midnight</f:case>
<f:case value="light">Light</f:case>
<f:case value="dark">Dark</f:case>
<f:defaultCase>{data.background_variant}</f:defaultCase>
</f:switch>
</span>
<f:if condition="{data.image_aspect_ratio} != 'auto'">
<span class="vitec-badge">⛶ {data.image_aspect_ratio -> f:format.raw()}</span>
</f:if>
<f:if condition="{data.text_alignment} != 'left'">
<span class="vitec-badge">
<f:switch expression="{data.text_alignment}">
<f:case value="center">↔ Center</f:case>
<f:case value="right">→ Right</f:case>
<f:defaultCase>{data.text_alignment}</f:defaultCase>
</f:switch>
</span>
</f:if>
<f:if condition="{data.border_style} != 'none'">
<span class="vitec-badge">▭ Border: {data.border_style}</span>
</f:if>
<f:if condition="{data.shadow} != 'none'">
<span class="vitec-badge">☁ Shadow: {data.shadow}</span>
</f:if>
<f:if condition="{data.card_link.url}">
<span class="vitec-badge">🔗 Whole-card link</span>
</f:if>
<f:if condition="{data.cssClass}">
<span class="vitec-badge">⌗ .{data.cssClass}</span>
</f:if>
<f:if condition="{data.debug}">
<span class="vitec-badge vitec-badge--warning">⚙ Debug ON</span>
</f:if>
</div>
</div>
</f:section>
</html>

View File

@@ -0,0 +1,3 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
</html>

View File

View File

@@ -1,91 +0,0 @@
<?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>

View File

@@ -1,110 +0,0 @@
<?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>

View File

View File

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 349 B

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

View File

Before

Width:  |  Height:  |  Size: 203 B

After

Width:  |  Height:  |  Size: 203 B

View File

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 202 B

View File

Before

Width:  |  Height:  |  Size: 447 B

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -6,7 +6,7 @@
"authors": [],
"license": "GPL-2.0-or-later",
"require": {
"typo3/cms-core": "^13.4"
"typo3/cms-core": "^13.4 || ^14.0"
},
"autoload": {
"psr-4": {

View File

@@ -11,7 +11,7 @@ $EM_CONF[$_EXTKEY] = [
'version' => '1.0.1',
'constraints' => [
'depends' => [
'typo3' => '13.4.0-13.4.99',
'typo3' => '13.4.0-14.9.99',
],
'conflicts' => [],
'suggests' => [],

55
packages/vitec/ext_localconf.php Executable file → Normal file
View File

@@ -1,8 +1,6 @@
<?php
defined('TYPO3') || die();
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
// Configure view template paths for the OG Image backend module
@@ -30,11 +28,6 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Backend\View\BackendLay
// Register custom VITEC CKEditor RTE preset
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['vitec'] = 'EXT:vitec/Configuration/RTE/Vitec.yaml';
// TSConfig für Seiten laden
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
"@import 'EXT:aifood/Configuration/page.tsconfig'"
);
(static function () {
// Register plugins
ExtensionUtility::configurePlugin(
@@ -146,52 +139,4 @@ $GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['vitec'] = 'EXT:vitec/Configuratio
]
);
// Register icons
$iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
$iconRegistry->registerIcon(
'vitec-plugin-productlist',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productlist.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-productshow',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-productshow.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-usecaseshow',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaseshow.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-marketshow',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketshow.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-solutionshow',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-solutionshow.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-usecaselist',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-usecaselist.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-downloadcard',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcard.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-downloadcardcollection',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-downloadcardcollection.svg']
);
$iconRegistry->registerIcon(
'vitec-plugin-datasheets',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-datasheets.svg']
);
})();

View File

@@ -11,48 +11,6 @@ ExtensionManagementUtility::addToAllTCAtypes(
'categories'
);
// Include Page TSConfig
ExtensionManagementUtility::addPageTSConfig(
'<INCLUDE_TYPOSCRIPT: source="FILE:EXT:vitec/Configuration/page.tsconfig">'
);
// Register the Downloadcard plugin
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Downloadcard',
'Download Card'
);
// Register the Simplecard plugin
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Simplecard',
'Simple Card'
);
// Register the Downloadcard plugin
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Vitec',
'Downloadcardcollection',
'Download Card Collection'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_productlist'] = 'pi_flexform';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
'vitec_productlist',
'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml'
);
// Add FlexForm for Downloadcard plugin
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_downloadcard'] = 'pi_flexform';
ExtensionManagementUtility::addPiFlexFormValue(
'vitec_downloadcard',
'FILE:EXT:vitec/Configuration/FlexForms/Downloadcard.xml'
);
// Add FlexForm for DownloadcardCollection plugin
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['vitec_downloadcardcollection'] = 'pi_flexform';
ExtensionManagementUtility::addPiFlexFormValue(
'vitec_downloadcardcollection',
'FILE:EXT:vitec/Configuration/FlexForms/Downloadcardcollection.xml'
);
// Include Page TSConfig (TYPO3 v14 compatible registration)
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = ($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '')
. "\n<INCLUDE_TYPOSCRIPT: source=\"FILE:EXT:vitec/Configuration/page.tsconfig\">";

0
packages/vitec/ext_tables.sql Executable file → Normal file
View File

View File

@@ -1,171 +0,0 @@
CREATE TABLE tx_vitec_domain_model_product (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
PRIMARY KEY (uid),
urltitle varchar(255) DEFAULT '' NOT NULL,
seotitle varchar(255) DEFAULT '' NOT NULL,
seometa text,
keywords text,
productimage int(11) DEFAULT '0' NOT NULL,
structureddata text,
teaser varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
applications text,
highlights text,
description text,
image INTEGER,
relatedimage INTEGER,
ogimage INTEGER,
legacy tinyint(4) DEFAULT '0' NOT NULL,
supportproduct tinyint(4) DEFAULT '0' NOT NULL,
subproduct tinyint(4) DEFAULT '0' NOT NULL,
cta varchar(255) DEFAULT '' NOT NULL,
sorting1 smallint(5) NOT NULL,
sorting2 smallint(5) NOT NULL,
sorting3 smallint(5) NOT NULL,
sorting4 smallint(5) NOT NULL,
sorting5 smallint(5) NOT NULL,
links text,
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL,
shortcut tinyint(4) DEFAULT '0' NOT NULL,
shortcutpid smallint(5) NOT NULL,
video varchar(255) DEFAULT '' NOT NULL,
key1 varchar(255) DEFAULT '' NOT NULL,
key2 varchar(255) DEFAULT '' NOT NULL,
key3 varchar(255) DEFAULT '' NOT NULL,
apptext1 varchar(255) DEFAULT '' NOT NULL,
apptext2 varchar(255) DEFAULT '' NOT NULL,
apptext3 varchar(255) DEFAULT '' NOT NULL,
productlayout int(11) DEFAULT '0' NOT NULL,
contentelement varchar(255) DEFAULT '' NOT NULL,
contentelementcta varchar(255) DEFAULT '' NOT NULL,
KEY parent (pid)
);
CREATE TABLE tx_vitec_domain_model_download (
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) NOT NULL DEFAULT '',
keywords varchar(255) NOT NULL DEFAULT '',
description text,
file int(11) unsigned NOT NULL DEFAULT '0',
sort1 varchar(255) NOT NULL DEFAULT '',
sort2 varchar(255) NOT NULL DEFAULT '',
sort3 varchar(255) NOT NULL DEFAULT '',
private_download smallint(1) unsigned NOT NULL DEFAULT '0',
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL,
icon varchar(255) NOT NULL DEFAULT '',
filepath varchar(255) NOT NULL DEFAULT '',
fileprefix varchar(255) NOT NULL DEFAULT '',
useolddl smallint(1) unsigned NOT NULL DEFAULT '0'
);
CREATE TABLE tx_vitec_product_download_mm (
uid_local int(11) DEFAULT '0' NOT NULL,
uid_foreign int(11) DEFAULT '0' NOT NULL,
sorting int(11) DEFAULT '0' NOT NULL,
sorting_foreign int(11) DEFAULT '0' NOT NULL,
KEY uid_local (uid_local),
KEY uid_foreign (uid_foreign)
);
CREATE TABLE tx_vitec_product_related_mm (
uid_local INT(11) NOT NULL,
uid_foreign INT(11) NOT NULL,
sorting INT(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid_local, uid_foreign)
);
CREATE TABLE tx_vitec_domain_model_usecase (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
description text,
caseimage INTEGER,
logoimage INTEGER,
singlepid varchar(255) DEFAULT '' NOT NULL,
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid)
);
CREATE TABLE tx_vitec_domain_model_solution (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
sys_language_uid int(11) DEFAULT '0' NOT NULL,
l10n_parent int(11) DEFAULT '0' NOT NULL,
l10n_diffsource mediumblob,
title varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
description text,
image int(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY language (l10n_parent, sys_language_uid)
);
CREATE TABLE tx_vitec_domain_model_market (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
sys_language_uid int(11) DEFAULT '0' NOT NULL,
l10n_parent int(11) DEFAULT '0' NOT NULL,
l10n_diffsource mediumblob,
title varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
description text,
image int(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY language (l10n_parent, sys_language_uid)
);
ALTER TABLE sys_category
ADD class VARCHAR(255) DEFAULT '' NOT NULL,
ADD filetype VARCHAR(255) DEFAULT '' NOT NULL,
ADD type VARCHAR(255) DEFAULT '' NOT NULL;
CREATE TABLE tt_content (
tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL
);

View File

@@ -1,172 +0,0 @@
CREATE TABLE tx_vitec_domain_model_product (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
PRIMARY KEY (uid),
urltitle varchar(255) DEFAULT '' NOT NULL,
seotitle varchar(255) DEFAULT '' NOT NULL,
seometa text,
keywords text,
productimage int(11) DEFAULT '0' NOT NULL,
structureddata text,
teaser varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
applications text,
highlights text,
description text,
image INTEGER,
relatedimage INTEGER,
ogimage INTEGER,
legacy tinyint(4) DEFAULT '0' NOT NULL,
supportproduct tinyint(4) DEFAULT '0' NOT NULL,
subproduct tinyint(4) DEFAULT '0' NOT NULL,
cta varchar(255) DEFAULT '' NOT NULL,
sorting1 smallint(5) NOT NULL,
sorting2 smallint(5) NOT NULL,
sorting3 smallint(5) NOT NULL,
sorting4 smallint(5) NOT NULL,
sorting5 smallint(5) NOT NULL,
links text,
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL,
shortcut tinyint(4) DEFAULT '0' NOT NULL,
shortcutpid smallint(5) NOT NULL,
video varchar(255) DEFAULT '' NOT NULL,
key1 varchar(255) DEFAULT '' NOT NULL,
key2 varchar(255) DEFAULT '' NOT NULL,
key3 varchar(255) DEFAULT '' NOT NULL,
apptext1 varchar(255) DEFAULT '' NOT NULL,
apptext2 varchar(255) DEFAULT '' NOT NULL,
apptext3 varchar(255) DEFAULT '' NOT NULL,
productlayout int(11) DEFAULT '0' NOT NULL,
contentelement varchar(255) DEFAULT '' NOT NULL,
contentelementcta varchar(255) DEFAULT '' NOT NULL,
videofile int(11) unsigned DEFAULT '0' NOT NULL,
KEY parent (pid)
);
CREATE TABLE tx_vitec_domain_model_download (
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) NOT NULL DEFAULT '',
keywords varchar(255) NOT NULL DEFAULT '',
description text,
file int(11) unsigned NOT NULL DEFAULT '0',
sort1 varchar(255) NOT NULL DEFAULT '',
sort2 varchar(255) NOT NULL DEFAULT '',
sort3 varchar(255) NOT NULL DEFAULT '',
private_download smallint(1) unsigned NOT NULL DEFAULT '0',
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
hideondatasheets smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonproducts smallint(5) unsigned DEFAULT '0' NOT NULL,
icon varchar(255) NOT NULL DEFAULT '',
filepath varchar(255) NOT NULL DEFAULT '',
fileprefix varchar(255) NOT NULL DEFAULT '',
useolddl smallint(1) unsigned NOT NULL DEFAULT '0'
);
CREATE TABLE tx_vitec_product_download_mm (
uid_local int(11) DEFAULT '0' NOT NULL,
uid_foreign int(11) DEFAULT '0' NOT NULL,
sorting int(11) DEFAULT '0' NOT NULL,
sorting_foreign int(11) DEFAULT '0' NOT NULL,
KEY uid_local (uid_local),
KEY uid_foreign (uid_foreign)
);
CREATE TABLE tx_vitec_product_related_mm (
uid_local INT(11) NOT NULL,
uid_foreign INT(11) NOT NULL,
sorting INT(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid_local, uid_foreign)
);
CREATE TABLE tx_vitec_domain_model_usecase (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
description text,
caseimage INTEGER,
logoimage INTEGER,
singlepid varchar(255) DEFAULT '' NOT NULL,
hideonapp smallint(5) unsigned DEFAULT '0' NOT NULL,
hideonwebsite smallint(5) unsigned DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid)
);
CREATE TABLE tx_vitec_domain_model_solution (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
sys_language_uid int(11) DEFAULT '0' NOT NULL,
l10n_parent int(11) DEFAULT '0' NOT NULL,
l10n_diffsource mediumblob,
title varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
description text,
image int(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY language (l10n_parent, sys_language_uid)
);
CREATE TABLE tx_vitec_domain_model_market (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
starttime int(11) DEFAULT '0' NOT NULL,
endtime int(11) DEFAULT '0' NOT NULL,
sys_language_uid int(11) DEFAULT '0' NOT NULL,
l10n_parent int(11) DEFAULT '0' NOT NULL,
l10n_diffsource mediumblob,
title varchar(255) DEFAULT '' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
teaser varchar(255) DEFAULT '' NOT NULL,
description text,
image int(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY language (l10n_parent, sys_language_uid)
);
ALTER TABLE sys_category
ADD class VARCHAR(255) DEFAULT '' NOT NULL,
ADD filetype VARCHAR(255) DEFAULT '' NOT NULL,
ADD type VARCHAR(255) DEFAULT '' NOT NULL;
CREATE TABLE tt_content (
tx_vitec_bg_variant VARCHAR(20) DEFAULT 'none' NOT NULL
);