Add Container Options

This commit is contained in:
2026-08-14 10:36:36 +02:00
parent f515de54ad
commit c720696781
23 changed files with 584 additions and 22 deletions

View File

@@ -76,6 +76,11 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
*/
private const CONTAINER_FIELDS = [
'tx_vitec_gap',
// Background settings belong to the container, not to what sits inside
// it. They carry non-empty defaults ('none', 'cover', 'center center'),
// so without this they would show up in every single child's `data`.
'tx_vitec_bg_variant', 'tx_vitec_bg_image', 'tx_vitec_bg_size',
'tx_vitec_bg_size_percent', 'tx_vitec_bg_position',
'tx_vitec_col1_align', 'tx_vitec_col1_justify',
'tx_vitec_col2_align', 'tx_vitec_col2_justify',
'tx_vitec_col3_align', 'tx_vitec_col3_justify',

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use Evomedien\Vitec\Service\UsecaseSerializer;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc emitting the background block of a VITEC container.
*
* The container elements are rendered entirely in TypoScript
* (Configuration/TypoScript/Headless/vitec_containers.typoscript), where plain
* fields are TEXT objects. A FAL image is not a plain field, so it comes through
* here — and going through `UsecaseSerializer::image()` rather than the headless
* FilesProcessor is deliberate: Clause 9.2 puts image serialisation in one place,
* so a background image has the same `{url, srcset, properties}` shape as every
* other image in this JSON. One shape for the front end instead of two.
*
* Emits nothing at all when no image is set — the whole `background` key then
* stays out of the payload, which is a clearer signal than an object with a null
* image and two orphaned CSS settings.
*
* `size` and `position` are stored as CSS-ready values (`cover`, `left top`, …).
* The single exception is `stretch`, which has no CSS keyword and the front end
* maps to `100% 100%`; see the TCA comment in tt_content_vitec_shared.php.
*
* Fail-soft per Clause 9.5: any error yields an empty string, never an exception
* that would take the surrounding content element down with it.
*/
class ContainerBackgroundRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 no longer assigns `$classObj->cObj` dynamically. The renderer is
* handed over through this setter, and only when the method exists at all -
* ContentObjectRenderer::callUserFunction() duck-types it with
* `is_callable([$classObj, 'setContentObjectRenderer'])`. Without the method
* `$this->cObj` stays null and the userFunc never sees its own record.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
try {
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row === null) {
return '';
}
$uid = (int)($row['uid'] ?? 0);
if ($uid <= 0) {
return '';
}
// No shortcut on the tx_vitec_bg_image counter: sys_file_reference is
// the single source of truth, and a counter that is stale or missing
// (schema not updated yet) would silently swallow a set image. One
// indexed query per container is cheaper than that class of bug.
$image = GeneralUtility::makeInstance(UsecaseSerializer::class)
->image($uid, 'tx_vitec_bg_image', 'tt_content');
if ($image === null) {
return '';
}
return (string)json_encode([
'image' => $image,
'size' => $this->resolveSize($row),
'position' => (string)($row['tx_vitec_bg_position'] ?? 'center center'),
]);
} catch (\Throwable $e) {
return '';
}
}
/**
* The stored size, resolved into something the front end can hand straight
* to CSS. `custom` is the only value that needs a second field; turning it
* into "80%" here keeps the promise that `size` is always usable as-is,
* instead of making every consumer look up a companion field.
*
* A custom size without a usable percentage falls back to `auto` - the CSS
* default - rather than emitting "0%" or the meaningless word "custom".
*
* @param array<string,mixed> $row
*/
private function resolveSize(array $row): string
{
$size = (string)($row['tx_vitec_bg_size'] ?? 'cover');
if ($size !== 'custom') {
return $size;
}
$percent = (int)($row['tx_vitec_bg_size_percent'] ?? 0);
return $percent > 0 ? $percent . '%' : 'auto';
}
}

View File

@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the VITEC customer logos as JSON (headless).
@@ -29,13 +30,28 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class CustomerlogosJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const TABLE = 'tx_vitec_domain_model_customer';
private const IMAGE_WIDTHS = [200, 400];
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_customerlogos') {
return $this->renderForRecord($row);
}

View File

@@ -13,6 +13,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render the event list (tx_vitec_domain_model_event) as JSON.
@@ -30,6 +31,21 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class EventlistJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
/**
* Parent of the region categories. Hard-coded like the other taxonomy roots
* in this extension (Annex B-5 tracks them); making it a FlexForm setting
@@ -42,7 +58,7 @@ class EventlistJsonRenderer
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_eventlist') {
return $this->renderForRecord($row);
}

View File

@@ -10,6 +10,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render ONE of the VITEC form plugins as JSON (headless).
@@ -25,10 +26,25 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class FormsJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && isset(FormDefinitions::CTYPE_MAP[(string)($row['CType'] ?? '')])) {
return $this->renderForRecord($row);
}

View File

@@ -27,12 +27,27 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
*/
class LocationsJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const TABLE = 'tx_vitec_domain_model_location';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_locationlist') {
return $this->renderForRecord($row);
}

View File

@@ -12,6 +12,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render all VITEC markets as JSON (headless).
@@ -38,13 +39,28 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class MarketListJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const TABLE = 'tx_vitec_domain_model_market';
private const CTYPE = 'vitec_marketlist';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === self::CTYPE) {
return $this->renderForRecord($row);
}

View File

@@ -14,6 +14,7 @@ use Evomedien\Vitec\Service\LinkResolver;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single market as JSON for headless output.
@@ -24,11 +25,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class MarketShowJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_marketshow') {
return $this->renderForRecord($row);
}

View File

@@ -12,6 +12,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the VITEC Card plugin as JSON (headless).
@@ -29,6 +30,21 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class ModelcardJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const MODEL_TABLES = [
'product' => 'tx_vitec_domain_model_product',
'story' => 'tx_vitec_domain_model_usecase',
@@ -39,7 +55,7 @@ class ModelcardJsonRenderer
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_modelcard') {
return $this->renderForRecord($row);
}

View File

@@ -14,6 +14,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Headless JSON renderer for EXT:news content elements.
@@ -25,6 +26,21 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
final class NewsJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const NEWS_CTYPES = [
'news_pi1',
'news_newsliststicky',
@@ -60,7 +76,7 @@ final class NewsJsonRenderer
public function render(string $content, array $conf): string
{
try {
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row !== null && in_array((string)($row['CType'] ?? ''), self::NEWS_CTYPES, true)) {
return $this->renderForRecord($row);
}

View File

@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render product list as JSON for headless.
@@ -30,11 +31,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class ProductListJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productlist') {
return $this->renderForRecord($row);
}

View File

@@ -17,6 +17,7 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Extbase\Service\ImageService;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single product as JSON for headless output.
@@ -28,11 +29,26 @@ use Doctrine\DBAL\ParameterType;
*/
class ProductShowJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productshow') {
return $this->renderForRecord($row);
}

View File

@@ -14,6 +14,7 @@ use Evomedien\Vitec\Service\LinkResolver;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single solution as JSON for headless output.
@@ -24,11 +25,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class SolutionShowJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_solutionshow') {
return $this->renderForRecord($row);
}

View File

@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the Success Story list as JSON (headless).
@@ -22,12 +23,27 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class UsecaseListJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const TABLE = 'tx_vitec_domain_model_usecase';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaselist') {
return $this->renderForRecord($row);
}

View File

@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render a single Success Story as JSON (headless).
@@ -21,12 +22,27 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class UsecaseShowJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
* ContentObjectRenderer::callUserFunction() duck-types it with
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
* $this->cObj stays null, the cObj branch of render() never fires and the
* call falls through to page discovery, which picks the FIRST element of
* this CType on the page rather than the one actually being rendered.
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
private const TABLE = 'tx_vitec_domain_model_usecase';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaseshow') {
return $this->renderForRecord($row);
}