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:
1 parent
64ea55d29d
commit
3aaf8fdad5
98 files changed
+5650
-10154
No files matched your search
@@ -0,0 +1,231 @@
|
||||
<?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 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'],
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user