From 115354190f2071427b995ffd778eada06342fe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sun, 28 Jun 2026 21:51:21 +0200 Subject: [PATCH 1/2] [TASK] DPL-158: Fix PHPStan level 8 reportings PHPStan at level 8 reported 49 errors in `Classes/`. They are resolved without behaviour changes by: * adding the missing array value-type annotations on parameters, properties and return types, * guarding nullable and `false` return values (`getRecord()`, `preg_split()`, `saveXML()`, parsed request body), * correcting invalid casts and redundant null-coalescing, and * asserting the concrete `RenderingContext` before calling `getRequest()` in the site preset ViewHelper. The Core13 PHPStan configuration only analysed `Classes/` and `Tests/`, leaving the version specific `Core13/` sources unchecked. `Core13/` is now added to the analysed paths, which surfaced and fixes three further errors there. The optional `EXT:lowlevel` integration references an event class that is not resolvable without the package, so `typo3/cms-lowlevel` is added to `require-dev` for static analysis and testing. Finally the stale `runTests.sh` help text is corrected: the TYPO3 default version label, the PHP `8.5` label and version list, and the examples that referenced the no longer supported PHP `8.1`. --- Build/Scripts/runTests.sh | 16 ++++----- Build/phpstan/Core13/phpstan.neon | 1 + Classes/Client/ClientFactory.php | 3 ++ Classes/Controller/CkEditorController.php | 14 +++++--- .../Enum/RephraseSupportedDeepLLanguage.php | 7 ++-- Classes/FieldType/AbstractFieldType.php | 3 ++ Classes/FieldType/FieldTypeInterface.php | 3 ++ Classes/FieldType/FieldTypeRegistry.php | 7 ++-- Classes/FieldType/Input.php | 2 +- Classes/FieldType/Text.php | 1 + Classes/Form/UserFunc/WriteSupport.php | 11 +++++- Classes/Hooks/WriteHook.php | 13 +++++-- Classes/Service/DeeplService.php | 13 ++++--- Classes/Service/HtmlParser.php | 35 +++++++++++++------ .../Backend/Site/WritePresetViewHelper.php | 9 +++-- ...WritePageViewRegistrationEventListener.php | 1 + Core13/Generator/WriteDropdownGenerator.php | 5 +-- composer.json | 1 + 18 files changed, 102 insertions(+), 43 deletions(-) diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 5594390..f2a961c 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -218,14 +218,14 @@ Options: -t <13> Only with -s composerInstall|composerInstallMin|composerInstallMax Specifies the TYPO3 CORE Version to be used - - 12: (default) use TYPO3 v13 + - 13: (default) use TYPO3 v13 - -p <8.1|8.2|8.3|8.4|8.5> + -p <8.2|8.3|8.4|8.5> Specifies the PHP minor version to be used - 8.2: (default) use PHP 8.2 - 8.3: use PHP 8.3 - 8.4: use PHP 8.4 - - 8.4: use PHP 8.5 + - 8.5: use PHP 8.5 -x Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance|acceptanceInstall Send information to host instance for test or system under test break points. This is especially @@ -255,22 +255,22 @@ Options: Show this help. Examples: - # Run all core unit tests using PHP 8.1 + # Run all core unit tests using PHP 8.2 ./Build/Scripts/runTests.sh ./Build/Scripts/runTests.sh -s unit # Run all core units tests and enable xdebug (have a PhpStorm listening on port 9003!) ./Build/Scripts/runTests.sh -x - # Run unit tests in phpunit verbose mode with xdebug on PHP 8.1 and filter for test canRetrieveValueWithGP - ./Build/Scripts/runTests.sh -x -p 8.1 -e "-v --filter canRetrieveValueWithGP" + # Run unit tests in phpunit verbose mode with xdebug on PHP 8.2 and filter for test canRetrieveValueWithGP + ./Build/Scripts/runTests.sh -x -p 8.2 -e "-v --filter canRetrieveValueWithGP" # Run functional tests in phpunit with a filtered test method name in a specified file # example will currently execute two tests, both of which start with the search term ./Build/Scripts/runTests.sh -s functional -e "--filter deleteContent" typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php - # Run functional tests on postgres with xdebug, php 8.1 and execute a restricted set of tests - ./Build/Scripts/runTests.sh -x -p 8.1 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication + # Run functional tests on postgres with xdebug, php 8.2 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.2 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication # Run functional tests on postgres 11 ./Build/Scripts/runTests.sh -s functional -d postgres -k 11 diff --git a/Build/phpstan/Core13/phpstan.neon b/Build/phpstan/Core13/phpstan.neon index f85bac5..20cf196 100644 --- a/Build/phpstan/Core13/phpstan.neon +++ b/Build/phpstan/Core13/phpstan.neon @@ -10,6 +10,7 @@ parameters: paths: - ../../../Classes/ + - ../../../Core13/ - ../../../Tests/ excludePaths: diff --git a/Classes/Client/ClientFactory.php b/Classes/Client/ClientFactory.php index d0f7d4e..c5be134 100644 --- a/Classes/Client/ClientFactory.php +++ b/Classes/Client/ClientFactory.php @@ -36,6 +36,9 @@ public function __construct( ) { } + /** + * @param array $options + */ public function buildDeeplClient(array $options = []): DeepLClient { if ($this->configuration->getApiKey() === '') { diff --git a/Classes/Controller/CkEditorController.php b/Classes/Controller/CkEditorController.php index eb3f590..b077694 100644 --- a/Classes/Controller/CkEditorController.php +++ b/Classes/Controller/CkEditorController.php @@ -50,13 +50,17 @@ public function deeplConfiguredAction(ServerRequestInterface $request): Response public function optimizeTextAction(ServerRequestInterface $request): ResponseInterface { $data = $request->getParsedBody(); - $splittedText = $this->htmlParser->splitHtml($data['text']); - foreach ($splittedText as $node => $text) { + $data = is_array($data) ? $data : []; + $text = (string)($data['text'] ?? ''); + $style = (string)($data['style'] ?? ''); + $tone = (string)($data['tone'] ?? ''); + $splittedText = $this->htmlParser->splitHtml($text); + foreach ($splittedText as $node => $value) { $optimizedText = $this->deeplService->rephraseText( - $data['text'], + $text, null, - RephraseWritingStyleDeepL::tryFrom($data['style']), - RephraseToneDeepL::tryFrom($data['tone']) + RephraseWritingStyleDeepL::tryFrom($style), + RephraseToneDeepL::tryFrom($tone) ); $splittedText[$node] = $optimizedText; } diff --git a/Classes/Domain/Enum/RephraseSupportedDeepLLanguage.php b/Classes/Domain/Enum/RephraseSupportedDeepLLanguage.php index b779886..4bb09f3 100644 --- a/Classes/Domain/Enum/RephraseSupportedDeepLLanguage.php +++ b/Classes/Domain/Enum/RephraseSupportedDeepLLanguage.php @@ -46,6 +46,9 @@ final class RephraseSupportedDeepLLanguage ], ]; + /** + * @return list + */ public static function getAllLanguages(): array { return array_keys(self::LANGUAGES); @@ -66,7 +69,7 @@ public static function isWritingStyleSupported(string $language): bool if (!array_key_exists($language, self::LANGUAGES)) { return false; } - return self::LANGUAGES[$language]['writing_style'] ?? false; + return self::LANGUAGES[$language]['writing_style']; } public static function isToneSupportedByLanguage(string $language): bool @@ -74,6 +77,6 @@ public static function isToneSupportedByLanguage(string $language): bool if (!array_key_exists($language, self::LANGUAGES)) { return false; } - return self::LANGUAGES[$language]['tone'] ?? false; + return self::LANGUAGES[$language]['tone']; } } diff --git a/Classes/FieldType/AbstractFieldType.php b/Classes/FieldType/AbstractFieldType.php index 68a6bfb..e5361af 100644 --- a/Classes/FieldType/AbstractFieldType.php +++ b/Classes/FieldType/AbstractFieldType.php @@ -6,6 +6,9 @@ abstract class AbstractFieldType implements FieldTypeInterface { + /** + * @param array $configuration + */ final public function __construct( protected readonly array $configuration, protected readonly string $table, diff --git a/Classes/FieldType/FieldTypeInterface.php b/Classes/FieldType/FieldTypeInterface.php index a6485db..f303400 100644 --- a/Classes/FieldType/FieldTypeInterface.php +++ b/Classes/FieldType/FieldTypeInterface.php @@ -6,6 +6,9 @@ interface FieldTypeInterface { + /** + * @param array $configuration + */ public function __construct( array $configuration, string $table, diff --git a/Classes/FieldType/FieldTypeRegistry.php b/Classes/FieldType/FieldTypeRegistry.php index 048d34e..3e90ad0 100644 --- a/Classes/FieldType/FieldTypeRegistry.php +++ b/Classes/FieldType/FieldTypeRegistry.php @@ -7,20 +7,23 @@ final class FieldTypeRegistry { /** - * @var array + * @var array> */ private static array $fieldTypes = [ 'input' => Input::class, 'text' => Text::class, ]; + /** + * @param array $tcaConfig + */ public static function getFieldProcessingTypeByRenderType( array $tcaConfig, string $table, string $fieldName, ?string $type = null ): FieldTypeInterface { - $renderType = $tcaConfig['renderType'] ?? $tcaConfig['type']; + $renderType = (string)($tcaConfig['renderType'] ?? $tcaConfig['type'] ?? ''); return new self::$fieldTypes[$renderType]( $tcaConfig, $table, diff --git a/Classes/FieldType/Input.php b/Classes/FieldType/Input.php index a6d6075..2fcfbfc 100644 --- a/Classes/FieldType/Input.php +++ b/Classes/FieldType/Input.php @@ -9,7 +9,7 @@ final class Input extends AbstractFieldType public function getTextForProcessing( string|int $value ): array { - return [$value]; + return [(string)$value]; } public function getValueForDatabase(array $processedText): int|string diff --git a/Classes/FieldType/Text.php b/Classes/FieldType/Text.php index f476389..e085646 100644 --- a/Classes/FieldType/Text.php +++ b/Classes/FieldType/Text.php @@ -14,6 +14,7 @@ final class Text extends AbstractFieldType public function getTextForProcessing( string|int $value ): array { + $value = (string)$value; if ($this->isRteField()) { $processing = $this->getHtmlParser()->splitHtml($value); } else { diff --git a/Classes/Form/UserFunc/WriteSupport.php b/Classes/Form/UserFunc/WriteSupport.php index 595ba40..44058a4 100644 --- a/Classes/Form/UserFunc/WriteSupport.php +++ b/Classes/Form/UserFunc/WriteSupport.php @@ -11,6 +11,9 @@ final class WriteSupport { + /** + * @param array $configuration + */ public function getSupportedLanguageForField(array &$configuration): void { foreach (RephraseSupportedDeepLLanguage::getAllLanguages() as $supportedLanguage) { @@ -21,6 +24,9 @@ public function getSupportedLanguageForField(array &$configuration): void } } + /** + * @param array $configuration + */ public function getSupportedToneForField(array &$configuration): void { foreach (RephraseToneDeepL::cases() as $supportedTone) { @@ -31,6 +37,9 @@ public function getSupportedToneForField(array &$configuration): void } } + /** + * @param array $configuration + */ public function getSupportedWritingStyleForField(array &$configuration): void { foreach (RephraseWritingStyleDeepL::cases() as $supportedWritingStyle) { @@ -42,7 +51,7 @@ public function getSupportedWritingStyleForField(array &$configuration): void } /** - * @param array{record?: array{deeplTargetLanguage?: array|string|null}} $params + * @param array{record?: array{deeplWriteLanguage?: array|string|null}} $params */ public function languageIsRephraseSupported(array $params, EvaluateDisplayConditions $conditions): bool { diff --git a/Classes/Hooks/WriteHook.php b/Classes/Hooks/WriteHook.php index 03d9fdb..84af4b5 100644 --- a/Classes/Hooks/WriteHook.php +++ b/Classes/Hooks/WriteHook.php @@ -43,14 +43,17 @@ public function processCmdmap( return; } - $recordId = $dataHandler->localize($table, $id, $value); + $recordId = $dataHandler->localize($table, (int)$id, $value); // localization went wrong if ($recordId === false) { return; } - $originalRecord = BackendUtility::getRecord($table, $id); - $translatedRecord = BackendUtility::getRecordLocalization($table, $id, $value); + $originalRecord = BackendUtility::getRecord($table, (int)$id); + if ($originalRecord === null) { + return; + } + $translatedRecord = BackendUtility::getRecordLocalization($table, (int)$id, $value); if ($translatedRecord === null) { return; @@ -66,6 +69,10 @@ public function processCmdmap( $commandIsProcessed = true; } + /** + * @param array $translatedRecord + * @param array $originalRecord + */ private function processRecordFieldsAndUpdate(string $table, array $translatedRecord, array $originalRecord, int|string $languageId): void { $pid = ($table === 'pages') ? $originalRecord['uid'] : $originalRecord['pid']; diff --git a/Classes/Service/DeeplService.php b/Classes/Service/DeeplService.php index b551ca5..3f32719 100644 --- a/Classes/Service/DeeplService.php +++ b/Classes/Service/DeeplService.php @@ -22,7 +22,7 @@ final class DeeplService { private const SENTENCE_SPLIT = '/([.?!]\s)/'; - private ?DeepLClient $deeplClient; + private DeepLClient $deeplClient; public function __construct( ClientFactory $clientFactory, @@ -76,10 +76,7 @@ public function rephraseText( } /** - * @param array{ - * writing_style?: string, - * tone?: string - * }|empty $options + * @param array $options * @throws DeepLException */ private function optimizeText( @@ -92,6 +89,9 @@ private function optimizeText( $targetLanguage, $options ); + if (is_array($rephrased)) { + $rephrased = $rephrased[0]; + } return (string)$rephrased; } @@ -107,6 +107,9 @@ private function splitTextToMaxSize(string $text): array } $sentences = preg_split(self::SENTENCE_SPLIT, $text, -1, PREG_SPLIT_DELIM_CAPTURE); + if ($sentences === false) { + return [$text]; + } $countResult = count($sentences); $snippets = []; diff --git a/Classes/Service/HtmlParser.php b/Classes/Service/HtmlParser.php index bd1b260..a55b82e 100644 --- a/Classes/Service/HtmlParser.php +++ b/Classes/Service/HtmlParser.php @@ -17,6 +17,9 @@ */ final class HtmlParser { + /** + * @return array + */ public function splitHtml(string $value): array { // The template tag is necessary! @@ -58,6 +61,9 @@ public function buildHtml(array $processedValue): string */ $template = $domResult->firstChild; $generatedHtml = $domResult->saveXML($template); + if ($generatedHtml === false) { + return ''; + } return str_replace([''], '', $generatedHtml); } @@ -69,13 +75,13 @@ public function buildHtml(array $processedValue): string * in the following structure: `|, * for example, p|2 means 2nd element in current level and HTML tag

. * + * @param DOMNodeList $nodeList * @return array */ private function xmlToArray(DOMNodeList $nodeList, string $parentNodeName = ''): array { $result = []; $i = 0; - /** @var DOMNode $child */ foreach ($nodeList as $node) { $nodeNameAndLevel = sprintf('%s%s|%d', $parentNodeName ? sprintf('%s>', $parentNodeName) : '', $node->nodeName, $i); if ($node->hasChildNodes()) { @@ -84,7 +90,7 @@ private function xmlToArray(DOMNodeList $nodeList, string $parentNodeName = ''): $this->xmlToArray($node->childNodes, $nodeNameAndLevel) ); } else { - $result[$nodeNameAndLevel] = $node->nodeValue; + $result[$nodeNameAndLevel] = (string)$node->nodeValue; } $i++; } @@ -94,40 +100,49 @@ private function xmlToArray(DOMNodeList $nodeList, string $parentNodeName = ''): /** * Unflattens the processed array and makes it associative * for further processing + * + * @param array $result + * @param list $entryPointLevel + * @return array */ - private function addToResult(array $result, $entryPointLevel, string $value): array + private function addToResult(array $result, array $entryPointLevel, string $value): array { $currentEntryPointLevel = array_shift($entryPointLevel); if ($currentEntryPointLevel === null) { - $result = [$value]; - return $result; + return [$value]; } [$nodeType, $countInLevel] = explode('|', $currentEntryPointLevel); - $result[$countInLevel][$nodeType] ??= []; - $result[$countInLevel][$nodeType] = $this->addToResult($result[$countInLevel][$nodeType], $entryPointLevel, $value); + $subResult = $result[$countInLevel][$nodeType] ?? []; + if (!is_array($subResult)) { + $subResult = []; + } + $result[$countInLevel][$nodeType] = $this->addToResult($subResult, $entryPointLevel, $value); return $result; } /** * Adds all found nodes recursively to the DOM + * + * @param array $currentProcessing */ private function addToDomRecursive(DOMNode $parentNode, array $currentProcessing): void { foreach ($currentProcessing as $processingNode) { if (!is_array($processingNode)) { // last node, text only - $parentNode->nodeValue = $processingNode; + $parentNode->nodeValue = is_scalar($processingNode) ? (string)$processingNode : ''; return; } - $currentNodeType = array_keys($processingNode)[0]; + $currentNodeType = (string)(array_keys($processingNode)[0] ?? ''); if ($currentNodeType === '#text') { $currentNode = new DOMText(); } else { try { $currentNode = new DOMElement($currentNodeType); - } catch (\DOMException $e) { + } catch (\DOMException) { // @todo add more DOMNode properties + continue; } } $parentNode->appendChild($currentNode); diff --git a/Classes/ViewHelpers/Backend/Site/WritePresetViewHelper.php b/Classes/ViewHelpers/Backend/Site/WritePresetViewHelper.php index ac09cb2..29463cf 100644 --- a/Classes/ViewHelpers/Backend/Site/WritePresetViewHelper.php +++ b/Classes/ViewHelpers/Backend/Site/WritePresetViewHelper.php @@ -8,6 +8,7 @@ use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; use WebVision\DeeplWrite\Domain\Enum\RephraseSupportedDeepLLanguage; use WebVision\DeeplWrite\Domain\Enum\RephraseToneDeepL; @@ -22,7 +23,7 @@ public function __construct( ) { } - public function initializeArguments() + public function initializeArguments(): void { $this->registerArgument('siteLanguageIds', 'string', 'Comma separated site languages', true); } @@ -31,7 +32,11 @@ public function render(): string { // @todo $siteLanguageIds is not used - ViewHelper argument make not sense. Should be rechecked. $siteLanguageIds = GeneralUtility::intExplode(',', $this->arguments['siteLanguageIds'], true); - $request = $this->renderingContext->getRequest(); + $renderingContext = $this->renderingContext; + if (!$renderingContext instanceof RenderingContext) { + return ''; + } + $request = $renderingContext->getRequest(); if (!$request instanceof ServerRequest) { return ''; } diff --git a/Core13/Event/Listener/DeeplWritePageViewRegistrationEventListener.php b/Core13/Event/Listener/DeeplWritePageViewRegistrationEventListener.php index 4be64f3..da371d6 100644 --- a/Core13/Event/Listener/DeeplWritePageViewRegistrationEventListener.php +++ b/Core13/Event/Listener/DeeplWritePageViewRegistrationEventListener.php @@ -95,6 +95,7 @@ public function __invoke(ModifyInjectVariablesViewHelperEvent $event): void } /** + * @param array $parameters * @throws RouteNotFoundException */ private function buildBackendRoute(string $route, array $parameters): string diff --git a/Core13/Generator/WriteDropdownGenerator.php b/Core13/Generator/WriteDropdownGenerator.php index 9ea756d..e34fa34 100644 --- a/Core13/Generator/WriteDropdownGenerator.php +++ b/Core13/Generator/WriteDropdownGenerator.php @@ -75,10 +75,6 @@ public function buildWriteDropdown( $output .= ''; } - if ($output === '') { - return ''; - } - return sprintf( '%s', htmlspecialchars($this->getLocalization()->sL('LLL:EXT:deepl_write/Resources/Private/Language/locallang.xlf:backend.label')), @@ -138,6 +134,7 @@ private function getLocalization(): LanguageService } /** + * @param array $parameters * @throws RouteNotFoundException */ private function buildBackendRoute(string $route, array $parameters): string diff --git a/composer.json b/composer.json index 1c1679f..1da4de3 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "phpstan/phpstan": "^1.12.33", "phpunit/phpunit": "^10.5", "saschaegerer/phpstan-typo3": "^1.10.2", + "typo3/cms-lowlevel": "^13.4", "typo3/testing-framework": "^8.3.1" }, "license": "GPL-2.0-or-later", From 3b00a95b06d6d5573430f6f78d0657c36251487f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sun, 28 Jun 2026 21:59:15 +0200 Subject: [PATCH 2/2] [TASK] DPL-158: Enable PHPStan in TYPO3 v13 CI The PHPStan analysis step in the TYPO3 v13 GitHub workflow was commented out. Now that the codebase passes PHPStan at level 8, the step is enabled so regressions are caught in CI. --- .github/workflows/testcore13.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testcore13.yml b/.github/workflows/testcore13.yml index a51805e..e036704 100644 --- a/.github/workflows/testcore13.yml +++ b/.github/workflows/testcore13.yml @@ -40,8 +40,8 @@ jobs: - name: "Find duplicate exception codes" run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s checkExceptionCodes" -# - name: "Run PHPStan" -# run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s phpstan" + - name: "Run PHPStan" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s phpstan" # testsuite: # name: all tests with core v13