From 108af3622305664d3a5a2acba5d9ae61de2aaa40 Mon Sep 17 00:00:00 2001 From: Sebastian Michaelsen Date: Thu, 26 Feb 2026 11:37:53 +0100 Subject: [PATCH] chore: drop TYPO3 v12 and upgrade dev toolchain Move the package baseline to TYPO3 v13, upgrade the testing/static-analysis stack to compatible major versions, and tighten helper typing so phpstan/phpunit/functional checks stay green. --- .github/phpstan.neon | 5 ++ .github/workflows/ci.yml | 2 +- .../WebcomponentContentObject.php | 23 +++---- .../AssertionFailedException.php | 4 +- .../Helpers/DateTimeFormatHelper.php | 8 ++- .../Helpers/InlineItemsHelper.php | 62 ++++++++++++++++--- Classes/DataProviding/Helpers/LabelHelper.php | 4 +- Classes/DataProviding/Helpers/LinkHelper.php | 6 +- Classes/Dto/InputData.php | 2 +- Classes/Rendering/ComponentRenderer.php | 16 +++-- composer.json | 18 +++--- 11 files changed, 105 insertions(+), 45 deletions(-) diff --git a/.github/phpstan.neon b/.github/phpstan.neon index 77b3689..754b49f 100644 --- a/.github/phpstan.neon +++ b/.github/phpstan.neon @@ -5,3 +5,8 @@ parameters: level: 9 paths: - ../Classes + ignoreErrors: + - identifier: trait.unused + path: ../Classes/DataProviding/Traits/ContentObjectRendererTrait.php + - identifier: trait.unused + path: ../Classes/DataProviding/Traits/RenderHtml.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55d845f..b5fb13f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - name: PHPUnit Tests uses: php-actions/phpunit@master with: - version: 10.5 + version: 11.5 bootstrap: vendor/autoload.php configuration: .github/phpunit.xml diff --git a/Classes/ContentObject/WebcomponentContentObject.php b/Classes/ContentObject/WebcomponentContentObject.php index eb40873..93a514d 100644 --- a/Classes/ContentObject/WebcomponentContentObject.php +++ b/Classes/ContentObject/WebcomponentContentObject.php @@ -6,7 +6,6 @@ use Psr\Log\LoggerInterface; use Sinso\Webcomponents\DataProviding\AssertionFailedException; -use Sinso\Webcomponents\DataProviding\ComponentInterface; use Sinso\Webcomponents\Dto\ComponentRenderingData; use Sinso\Webcomponents\Dto\InputData; use Sinso\Webcomponents\Rendering\ComponentRenderer; @@ -24,12 +23,15 @@ public function __construct( */ public function render($conf = []): string { + $contentObjectRenderer = $this->getContentObjectRenderer(); + /** @var array $record */ + $record = $contentObjectRenderer->data; + $inputData = new InputData( - $this->cObj?->data ?? [], - $this->cObj?->getCurrentTable() ?? '', + $record, + $contentObjectRenderer->getCurrentTable(), ); - $contentObjectRenderer = $this->getContentObjectRenderer(); if (is_array($conf['additionalInputData.'] ?? null)) { // apply stdWrap to all additionalInputData properties foreach ($conf['additionalInputData.'] as $key => $value) { @@ -44,9 +46,8 @@ public function render($conf = []): string $inputData->additionalData = $conf['additionalInputData.']; } $componentClassName = $contentObjectRenderer->stdWrapValue('component', $conf, null); - if (!empty($componentClassName)) { + if (is_string($componentClassName) && $componentClassName !== '') { try { - /** @var class-string $componentClassName */ $componentRenderingData = $this->componentRenderer->evaluateComponent($inputData, $componentClassName, $contentObjectRenderer); } catch (AssertionFailedException $e) { $this->logger->info('Component evaluation failed', ['conf' => $conf, 'data' => $inputData->record, 'exception' => $e]); @@ -56,7 +57,7 @@ public function render($conf = []): string $componentRenderingData = new ComponentRenderingData(); } - $componentRenderingData = $this->evaluateTypoScriptConfiguration($componentRenderingData, $conf); + $componentRenderingData = $this->evaluateTypoScriptConfiguration($componentRenderingData, $contentObjectRenderer, $conf); $markup = $this->componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer); @@ -71,17 +72,17 @@ public function render($conf = []): string /** * @param array $conf */ - private function evaluateTypoScriptConfiguration(ComponentRenderingData $componentRenderingData, array $conf): ComponentRenderingData + private function evaluateTypoScriptConfiguration(ComponentRenderingData $componentRenderingData, \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObjectRenderer, array $conf): ComponentRenderingData { if (is_array($conf['properties.'] ?? null)) { foreach ($conf['properties.'] as $key => $value) { - if (is_array($value)) { + if (!is_scalar($value)) { continue; } - $componentRenderingData = $componentRenderingData->withTagProperty($key, $this->cObj?->stdWrap($value, $conf['properties.'][$key . '.'] ?? [])); + $componentRenderingData = $componentRenderingData->withTagProperty((string)$key, $contentObjectRenderer->stdWrap((string)$value, $conf['properties.'][$key . '.'] ?? [])); } } - $tagName = $this->cObj?->stdWrapValue('tagName', $conf); + $tagName = $contentObjectRenderer->stdWrapValue('tagName', $conf); if (is_string($tagName) && $tagName !== '') { return $componentRenderingData->withTagName($tagName); } diff --git a/Classes/DataProviding/AssertionFailedException.php b/Classes/DataProviding/AssertionFailedException.php index f640823..85f1804 100644 --- a/Classes/DataProviding/AssertionFailedException.php +++ b/Classes/DataProviding/AssertionFailedException.php @@ -8,7 +8,9 @@ class AssertionFailedException extends \RuntimeException { public function getRenderingPlaceholder(): string { - if ($GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] ?? false) { + /** @var array{FE?: array{debug?: bool}} $confVars */ + $confVars = $GLOBALS['TYPO3_CONF_VARS'] ?? []; + if ($confVars['FE']['debug'] ?? false) { return ''; } return ''; diff --git a/Classes/DataProviding/Helpers/DateTimeFormatHelper.php b/Classes/DataProviding/Helpers/DateTimeFormatHelper.php index a84e867..2ddf36a 100644 --- a/Classes/DataProviding/Helpers/DateTimeFormatHelper.php +++ b/Classes/DataProviding/Helpers/DateTimeFormatHelper.php @@ -23,11 +23,13 @@ public function formatTime(\DateTimeInterface|int $timestamp, int $dateType = \I private function getLocaleFromRequest(): ?Locale { - return $this->getRequest()->getAttribute('language')?->getLocale(); + return $this->getRequest()?->getAttribute('language')?->getLocale(); } - private function getRequest(): ServerRequestInterface + private function getRequest(): ?ServerRequestInterface { - return $GLOBALS['TYPO3_REQUEST']; + /** @var ServerRequestInterface|null $request */ + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + return $request; } } diff --git a/Classes/DataProviding/Helpers/InlineItemsHelper.php b/Classes/DataProviding/Helpers/InlineItemsHelper.php index 6814534..18385d3 100644 --- a/Classes/DataProviding/Helpers/InlineItemsHelper.php +++ b/Classes/DataProviding/Helpers/InlineItemsHelper.php @@ -25,7 +25,7 @@ public function __construct( /** * @param array $parentRecord - * @return list> + * @return list> */ public function loadInlineItems(array $parentRecord, string $inlineFieldName, string $parentTable = 'tt_content'): array { @@ -49,8 +49,8 @@ public function loadInlineItems(array $parentRecord, string $inlineFieldName, st if (!is_string($foreignField) || $foreignField === '') { $foreignField = null; } - $foreignTableTca = $GLOBALS['TCA'][$foreignTable] ?? []; - $foreignSortby = $inlineFieldConfig['foreign_sortby'] ?? $foreignTableTca['ctrl']['sortby'] ?? null; + $foreignTableCtrl = $this->getTableCtrl($foreignTable); + $foreignSortby = $inlineFieldConfig['foreign_sortby'] ?? $foreignTableCtrl['sortby'] ?? null; if (!is_string($foreignSortby) || $foreignSortby === '') { $foreignSortby = null; } @@ -68,7 +68,7 @@ public function loadInlineItems(array $parentRecord, string $inlineFieldName, st } $constraints['uidList'] = $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($itemsUidList, ArrayParameterType::INTEGER)); } - $languageField = $foreignTableTca['ctrl']['languageField'] ?? null; + $languageField = $foreignTableCtrl['languageField'] ?? null; if (is_string($languageField) && $languageField !== '') { $constraints['language'] = $queryBuilder->expr()->in($languageField, $queryBuilder->createNamedParameter([-1, $parentRecord['sys_language_uid'] ?? 0], ArrayParameterType::INTEGER)); } @@ -80,11 +80,11 @@ public function loadInlineItems(array $parentRecord, string $inlineFieldName, st $processedItems = []; foreach ($queriedItems as $item) { // workspace overlay: - $this->pageRepository->versionOL($foreignTable, $item); - if ($item === false) { + $workspaceItem = $this->applyVersionOverlay($foreignTable, $item); + if (!is_array($workspaceItem)) { continue; } - $processedItems[] = $item; + $processedItems[] = $workspaceItem; } if (empty($foreignField)) { @@ -101,13 +101,55 @@ public function loadInlineItems(array $parentRecord, string $inlineFieldName, st } /** - * @return array{config?: array} + * @param array $item + * @return mixed + */ + private function applyVersionOverlay(string $table, array $item): mixed + { + $this->pageRepository->versionOL($table, $item); + return $item; + } + + /** + * @return array + */ + private function getTableCtrl(string $table): array + { + $tableTca = $this->getGlobalTca()[$table] ?? null; + if (!is_array($tableTca)) { + return []; + } + + $ctrl = $tableTca['ctrl'] ?? null; + return is_array($ctrl) ? $ctrl : []; + } + + /** + * @return array + */ + private function getGlobalTca(): array + { + $tca = $GLOBALS['TCA'] ?? null; + return is_array($tca) ? $tca : []; + } + + /** + * @return array{config?: array} */ protected function getInlineFieldTca(string $inlineFieldName, string $localTableName = 'tt_content'): array { - if (!isset($GLOBALS['TCA'][$localTableName]['columns'][$inlineFieldName])) { + $tableTca = $this->getGlobalTca()[$localTableName] ?? null; + $columns = is_array($tableTca) ? ($tableTca['columns'] ?? null) : null; + $fieldTca = is_array($columns) ? ($columns[$inlineFieldName] ?? null) : null; + if (!is_array($fieldTca)) { throw new \Exception('Tried to process inline records for non existing field ' . $localTableName . '.' . $inlineFieldName, 1587038305); } - return $GLOBALS['TCA'][$localTableName]['columns'][$inlineFieldName]; + + $config = $fieldTca['config'] ?? null; + if (!is_array($config)) { + return []; + } + + return ['config' => $config]; } } diff --git a/Classes/DataProviding/Helpers/LabelHelper.php b/Classes/DataProviding/Helpers/LabelHelper.php index e6e924f..6272d7c 100644 --- a/Classes/DataProviding/Helpers/LabelHelper.php +++ b/Classes/DataProviding/Helpers/LabelHelper.php @@ -45,6 +45,8 @@ private function isFrontendTypoScriptAvailable(): bool private function getRequest(): ?ServerRequestInterface { - return $GLOBALS['TYPO3_REQUEST'] ?? null; + /** @var ServerRequestInterface|null $request */ + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + return $request; } } diff --git a/Classes/DataProviding/Helpers/LinkHelper.php b/Classes/DataProviding/Helpers/LinkHelper.php index 88d2526..51271ba 100644 --- a/Classes/DataProviding/Helpers/LinkHelper.php +++ b/Classes/DataProviding/Helpers/LinkHelper.php @@ -52,10 +52,12 @@ private function htmlSanitizationIsActive(ContentObjectRenderer $contentObjectRe ['parseFunc' => '< lib.parseFunc'], 'parseFunc' ); - if (empty($configuration['parseFunc.']['htmlSanitize'])) { + + $htmlSanitize = $configuration['parseFunc.']['htmlSanitize'] ?? null; + if ($htmlSanitize === null) { return true; } - return $configuration['parseFunc.']['htmlSanitize'] !== '0'; + return (string)$htmlSanitize !== '0'; } } diff --git a/Classes/Dto/InputData.php b/Classes/Dto/InputData.php index e9e524c..aa0162f 100644 --- a/Classes/Dto/InputData.php +++ b/Classes/Dto/InputData.php @@ -10,7 +10,7 @@ final class InputData { /** - * @param array $record + * @param array $record * @param array $additionalData */ public function __construct( diff --git a/Classes/Rendering/ComponentRenderer.php b/Classes/Rendering/ComponentRenderer.php index 3f6bdc6..72ac7a0 100644 --- a/Classes/Rendering/ComponentRenderer.php +++ b/Classes/Rendering/ComponentRenderer.php @@ -32,14 +32,18 @@ public function renderComponent(ComponentRenderingData $componentRenderingData, return $this->renderMarkup($event->getComponentRenderingData(), $tagBuilder); } - /** - * @param class-string $componentClassName - */ public function evaluateComponent(InputData $inputData, string $componentClassName, ?ContentObjectRenderer $contentObjectRenderer = null): ComponentRenderingData { - /** @var ComponentInterface $component */ + if (!class_exists($componentClassName)) { + throw new AssertionFailedException( + 'Configured component class "' . $componentClassName . '" does not exist', + 1729064011 + ); + } + $component = GeneralUtility::makeInstance($componentClassName); - $this->assert($component instanceof ComponentInterface, 'Component must implement ComponentInterface'); + $this->assert($component instanceof ComponentInterface, 'Configured component class "' . $componentClassName . '" must implement ' . ComponentInterface::class); + /** @var ComponentInterface $component */ if ($contentObjectRenderer === null) { $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $contentObjectRenderer->start([]); @@ -52,7 +56,7 @@ public function evaluateComponent(InputData $inputData, string $componentClassNa ArrayUtility::removeNullValuesRecursive($componentRenderingData->getTagProperties()) ); - $event = new ComponentEvaluated($componentRenderingData, $contentObjectRenderer, $inputData, $componentClassName); + $event = new ComponentEvaluated($componentRenderingData, $contentObjectRenderer, $inputData, get_class($component)); $this->eventDispatcher->dispatch($event); return $event->getComponentRenderingData(); diff --git a/composer.json b/composer.json index 109c674..fe95a72 100644 --- a/composer.json +++ b/composer.json @@ -15,17 +15,17 @@ }, "require": { "ext-json": "*", - "typo3/cms-core": "^12.0 || ^13.0", - "typo3/cms-frontend": "*" + "typo3/cms-core": "^13.4", + "typo3/cms-frontend": "^13.4" }, "require-dev": { - "phpstan/phpstan": "^1.11.9", - "saschaegerer/phpstan-typo3": "^1.10", - "friendsofphp/php-cs-fixer": "^3.61", - "phpunit/phpunit": "^10.5.35", + "phpstan/phpstan": "^2.1", + "saschaegerer/phpstan-typo3": "^2.1", + "friendsofphp/php-cs-fixer": "^3.94", + "phpunit/phpunit": "^11.5", "typo3/coding-standards": "^0.8.0", - "typo3/testing-framework": "^8.2.3", - "ssch/typo3-rector": "^2.6" + "typo3/testing-framework": "^9.3", + "ssch/typo3-rector": "^3.12" }, "suggest": { "contentblocks/content-blocks": "Define webcomponents as content blocks" @@ -40,7 +40,7 @@ "@rector" ], "php-cs-fixer": "php-cs-fixer fix --config=./.github/.php-cs-fixer.dist.php", - "phpstan": "phpstan analyse --configuration=./.github/phpstan.neon", + "phpstan": "phpstan analyse --configuration=./.github/phpstan.neon --memory-limit=1G", "rector": "rector process --config=./.github/rector.php" } }