From dba154e5728eb616d623768cae9f70fe26e6ddd2 Mon Sep 17 00:00:00 2001 From: khaccount Date: Fri, 29 May 2026 11:14:02 +0200 Subject: [PATCH] Zwischenstand vom 29.05.2026 --- composer.lock | 698 +++++++++--------- .../Classes/Controller/OgImageController.php | 152 ++++ .../ContainerChildrenProcessor.php | 70 +- ...erChildrenProcessor.php.bak.20260518160048 | 157 ++++ .../Service/OgImageGeneratorService.php | 419 +++++++++++ .../UserFunc/ProductListJsonRenderer.php | 200 ++--- ...uctListJsonRenderer.php.bak.20260518160048 | 499 +++++++++++++ .../UserFunc/ProductShowJsonRenderer.php | 426 +++++++---- ...uctShowJsonRenderer.php.bak.20260518145244 | 366 +++++++++ ...uctShowJsonRenderer.php.bak.20260518160048 | 512 +++++++++++++ .../UserFunc/UsecaseListJsonRenderer.php | 223 ++++++ ...aseListJsonRenderer.php.bak.20260518160048 | 244 ++++++ .../UserFunc/UsecaseShowJsonRenderer.php | 265 +++++++ ...aseShowJsonRenderer.php.bak.20260518160048 | 271 +++++++ .../vitec/Configuration/Backend/Modules.php | 34 + .../Configuration/FlexForms/Container.xml | 22 + .../Configuration/FlexForms/Usecaselist.xml | 27 +- .../Usecaselist.xml.bak.20260518153402 | 29 + packages/vitec/Configuration/Icons.php | 4 + .../Sets/Vitecset/setup.typoscript | 62 +- .../setup.typoscript.bak.20260518152905 | 44 ++ .../setup.typoscript.bak.20260518154740 | 82 ++ .../Overrides/tt_content_vitec_container.php | 69 ++ ...ent_vitec_container.php.bak.20260518151017 | 64 ++ .../Headless/vitec_containers.typoscript | 11 + ...c_containers.typoscript.bak.20260518150609 | 89 +++ .../Private/Language/locallang_containers.xlf | 22 + ...ocallang_containers.xlf.bak.20260518150609 | 91 +++ ...ocallang_containers.xlf.bak.20260518151017 | 110 +++ .../Private/Language/locallang_ogimage.xlf | 16 + .../Private/Layouts/Backend/Default.html | 2 +- .../Private/Templates/OgImage/Index.html | 288 ++++++++ .../Public/Icons/vitec-container.svg | 6 + .../Resources/Public/Icons/vitec-ogimage.svg | 7 + .../Public/Javascript/og-image-preview.js | 173 +++++ packages/vitec/ext_localconf.php | 8 + 36 files changed, 5113 insertions(+), 649 deletions(-) create mode 100644 packages/vitec/Classes/Controller/OgImageController.php create mode 100755 packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 create mode 100644 packages/vitec/Classes/Service/OgImageGeneratorService.php create mode 100755 packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 create mode 100755 packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 create mode 100755 packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 create mode 100755 packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php create mode 100755 packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 create mode 100755 packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php create mode 100755 packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 create mode 100644 packages/vitec/Configuration/Backend/Modules.php create mode 100755 packages/vitec/Configuration/FlexForms/Container.xml create mode 100755 packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 create mode 100755 packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 create mode 100755 packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 create mode 100755 packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php create mode 100755 packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php.bak.20260518151017 create mode 100755 packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript.bak.20260518150609 create mode 100755 packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 create mode 100755 packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 create mode 100644 packages/vitec/Resources/Private/Language/locallang_ogimage.xlf create mode 100644 packages/vitec/Resources/Private/Templates/OgImage/Index.html create mode 100755 packages/vitec/Resources/Public/Icons/vitec-container.svg create mode 100644 packages/vitec/Resources/Public/Icons/vitec-ogimage.svg create mode 100644 packages/vitec/Resources/Public/Javascript/og-image-preview.js diff --git a/composer.lock b/composer.lock index 7b2e3c2..da1912c 100644 --- a/composer.lock +++ b/composer.lock @@ -284,16 +284,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.11", + "version": "1.5.12", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "68ff39175e8e94a4bb1d259407ce51a6a60f09e6" + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/68ff39175e8e94a4bb1d259407ce51a6a60f09e6", - "reference": "68ff39175e8e94a4bb1d259407ce51a6a60f09e6", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", "shasum": "" }, "require": { @@ -340,7 +340,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.11" + "source": "https://github.com/composer/ca-bundle/tree/1.5.12" }, "funding": [ { @@ -352,7 +352,7 @@ "type": "github" } ], - "time": "2026-03-30T09:16:10+00:00" + "time": "2026-05-19T11:26:22+00:00" }, { "name": "composer/class-map-generator", @@ -425,16 +425,16 @@ }, { "name": "composer/composer", - "version": "2.9.7", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113" + "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/82a2fbd1372a98d7915cfb092acf05207d9b4113", - "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113", + "url": "https://api.github.com/repos/composer/composer/zipball/c13824d95608b15913a7c0def0a3dea4474b71fc", + "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc", "shasum": "" }, "require": { @@ -487,7 +487,7 @@ ] }, "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.10-dev" } }, "autoload": { @@ -522,7 +522,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.9.7" + "source": "https://github.com/composer/composer/tree/2.10.0" }, "funding": [ { @@ -534,7 +534,7 @@ "type": "github" } ], - "time": "2026-04-14T11:31:52+00:00" + "time": "2026-05-28T09:22:08+00:00" }, { "name": "composer/metadata-minifier", @@ -1638,16 +1638,16 @@ }, { "name": "friendsoftypo3/content-blocks", - "version": "1.5.2", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/FriendsOfTYPO3/content-blocks.git", - "reference": "1a940c40345f76e320ef527de3a558b2c6341d38" + "reference": "a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/1a940c40345f76e320ef527de3a558b2c6341d38", - "reference": "1a940c40345f76e320ef527de3a558b2c6341d38", + "url": "https://api.github.com/repos/FriendsOfTYPO3/content-blocks/zipball/a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca", + "reference": "a95e8dfc32d1f8d52430ee04b0e22dbecd60e5ca", "shasum": "" }, "require": { @@ -1682,7 +1682,7 @@ "typo3/cms-rte-ckeditor": "^13.4.19", "typo3/cms-tstemplate": "^13.4.19", "typo3/cms-workspaces": "^13.4.19", - "typo3/coding-standards": "^0.8", + "typo3/coding-standards": "0.8.x-dev", "typo3/testing-framework": "^9" }, "type": "typo3-cms-extension", @@ -1719,24 +1719,25 @@ "issues": "https://github.com/FriendsOfTYPO3/content-blocks/issues", "source": "https://github.com/FriendsOfTYPO3/content-blocks" }, - "time": "2026-04-30T07:01:48+00:00" + "time": "2026-05-26T09:43:56+00:00" }, { "name": "friendsoftypo3/headless", - "version": "v4.7.3", + "version": "v4.8.0", "source": { "type": "git", "url": "https://github.com/TYPO3-Headless/headless.git", - "reference": "3e9175a9e384f1b5b529e24e27d25c419b3ab711" + "reference": "971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-Headless/headless/zipball/3e9175a9e384f1b5b529e24e27d25c419b3ab711", - "reference": "3e9175a9e384f1b5b529e24e27d25c419b3ab711", + "url": "https://api.github.com/repos/TYPO3-Headless/headless/zipball/971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9", + "reference": "971a7c4eb77edfdb0afd09ed37de7f30fe06e6f9", "shasum": "" }, "require": { "ext-json": "*", + "php": ">= 8.2", "typo3/cms-core": "^12.4 || ^13.4", "typo3/cms-install": "^12.4 || ^13.4" }, @@ -1800,7 +1801,7 @@ ], "support": { "issues": "https://github.com/TYPO3-Headless/headless/issues", - "source": "https://github.com/TYPO3-Headless/headless/tree/v4.7.3" + "source": "https://github.com/TYPO3-Headless/headless/tree/v4.8.0" }, "funding": [ { @@ -1808,20 +1809,20 @@ "type": "custom" } ], - "time": "2025-09-09T11:03:45+00:00" + "time": "2026-05-08T13:50:24+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.10.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", "shasum": "" }, "require": { @@ -1839,8 +1840,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.4", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1918,7 +1920,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.10.5" }, "funding": [ { @@ -1934,20 +1936,20 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-05-27T11:53:46+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", "shasum": "" }, "require": { @@ -1955,7 +1957,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -2001,7 +2003,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.4.1" }, "funding": [ { @@ -2017,20 +2019,20 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-05-20T22:57:30+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.9.0", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7c1472269227dc6f18930bd903d7a88fe6c52130", + "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130", "shasum": "" }, "require": { @@ -2045,9 +2047,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", + "http-interop/http-factory-tests": "1.1.0", "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2118,7 +2120,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.10.3" }, "funding": [ { @@ -2134,7 +2136,7 @@ "type": "tidelift" } ], - "time": "2026-03-10T16:41:02+00:00" + "time": "2026-05-27T11:48:20+00:00" }, { "name": "itplusx/headless-container", @@ -3902,16 +3904,16 @@ }, { "name": "symfony/cache", - "version": "v8.0.10", + "version": "v8.0.13", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "8ff96cde73684bfa32b702f5cff1eb83b1fac429" + "reference": "75f92239836ce08bce4d19f2734737dae4276fe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/8ff96cde73684bfa32b702f5cff1eb83b1fac429", - "reference": "8ff96cde73684bfa32b702f5cff1eb83b1fac429", + "url": "https://api.github.com/repos/symfony/cache/zipball/75f92239836ce08bce4d19f2734737dae4276fe9", + "reference": "75f92239836ce08bce4d19f2734737dae4276fe9", "shasum": "" }, "require": { @@ -3977,7 +3979,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.0.10" + "source": "https://github.com/symfony/cache/tree/v8.0.13" }, "funding": [ { @@ -3997,7 +3999,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T08:24:00+00:00" + "time": "2026-05-24T08:44:11+00:00" }, { "name": "symfony/cache-contracts", @@ -4237,16 +4239,16 @@ }, { "name": "symfony/console", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d7d2b64a45a89d607865927b176fa51c33ddbb58", - "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -4311,7 +4313,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.9" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -4331,20 +4333,20 @@ "type": "tidelift" } ], - "time": "2026-04-22T15:21:55+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.10", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" + "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f299e20ce983be6c0744952533c6dfeaaa1448e2", + "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2", "shasum": "" }, "require": { @@ -4395,7 +4397,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.13" }, "funding": [ { @@ -4415,7 +4417,7 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:55:30+00:00" + "time": "2026-05-20T14:07:29+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4799,16 +4801,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.9", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/dcd8f96bcdc0f128ec406c765cc066c6035d1be3", - "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -4845,7 +4847,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.9" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -4865,7 +4867,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", @@ -4937,16 +4939,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + "reference": "bc354f47c62301e990b7874fa662326368508e2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", "shasum": "" }, "require": { @@ -4995,7 +4997,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" }, "funding": [ { @@ -5015,20 +5017,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.8", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62" + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62", - "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", "shasum": "" }, "require": { @@ -5079,7 +5081,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.8" + "source": "https://github.com/symfony/mailer/tree/v7.4.12" }, "funding": [ { @@ -5099,20 +5101,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/messenger", - "version": "v7.4.10", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "8538bd43f3c928ab3400250b1e973698d28286fd" + "reference": "906387986caecc10b2ad2e85f715d834e5133a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/8538bd43f3c928ab3400250b1e973698d28286fd", - "reference": "8538bd43f3c928ab3400250b1e973698d28286fd", + "url": "https://api.github.com/repos/symfony/messenger/zipball/906387986caecc10b2ad2e85f715d834e5133a04", + "reference": "906387986caecc10b2ad2e85f715d834e5133a04", "shasum": "" }, "require": { @@ -5173,7 +5175,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v7.4.10" + "source": "https://github.com/symfony/messenger/tree/v7.4.12" }, "funding": [ { @@ -5193,20 +5195,20 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:21:16+00:00" + "time": "2026-05-19T07:02:47+00:00" }, { "name": "symfony/mime", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2d550c4758ba4c47519a6667c36553d535705b0c" + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2d550c4758ba4c47519a6667c36553d535705b0c", - "reference": "2d550c4758ba4c47519a6667c36553d535705b0c", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", "shasum": "" }, "require": { @@ -5262,7 +5264,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.9" + "source": "https://github.com/symfony/mime/tree/v7.4.13" }, "funding": [ { @@ -5282,7 +5284,7 @@ "type": "tidelift" } ], - "time": "2026-04-29T13:21:53+00:00" + "time": "2026-05-23T16:22:37+00:00" }, { "name": "symfony/options-resolver", @@ -5440,16 +5442,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -5498,7 +5500,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -5518,20 +5520,20 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -5585,7 +5587,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -5605,20 +5607,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.37.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -5670,7 +5672,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -5690,20 +5692,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { @@ -5755,7 +5757,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -5775,7 +5777,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php73", @@ -5943,16 +5945,16 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -5999,7 +6001,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" }, "funding": [ { @@ -6019,20 +6021,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:45:58+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + "reference": "8339098cae28673c15cce00d80734af0453054e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", + "reference": "8339098cae28673c15cce00d80734af0453054e2", "shasum": "" }, "require": { @@ -6079,7 +6081,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" }, "funding": [ { @@ -6099,20 +6101,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -6159,7 +6161,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -6179,7 +6181,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T18:47:49+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-uuid", @@ -6266,16 +6268,16 @@ }, { "name": "symfony/process", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -6307,7 +6309,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.8" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -6327,7 +6329,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/property-access", @@ -6502,16 +6504,16 @@ }, { "name": "symfony/rate-limiter", - "version": "v7.4.10", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "778c5239c7fd6bf9b886dedf3d84ddb156ddb888" + "reference": "8b162768544e5a8895c52161d63c999aca91f4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/778c5239c7fd6bf9b886dedf3d84ddb156ddb888", - "reference": "778c5239c7fd6bf9b886dedf3d84ddb156ddb888", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/8b162768544e5a8895c52161d63c999aca91f4a9", + "reference": "8b162768544e5a8895c52161d63c999aca91f4a9", "shasum": "" }, "require": { @@ -6552,7 +6554,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.4.10" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.13" }, "funding": [ { @@ -6572,20 +6574,20 @@ "type": "tidelift" } ], - "time": "2026-05-04T13:25:50+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/routing", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "287771d8bc86eacb30678dd10eda6c64a859951f" + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/287771d8bc86eacb30678dd10eda6c64a859951f", - "reference": "287771d8bc86eacb30678dd10eda6c64a859951f", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", "shasum": "" }, "require": { @@ -6637,7 +6639,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.9" + "source": "https://github.com/symfony/routing/tree/v7.4.13" }, "funding": [ { @@ -6657,7 +6659,7 @@ "type": "tidelift" } ], - "time": "2026-04-22T15:21:55+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/service-contracts", @@ -6748,16 +6750,16 @@ }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.0.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "f2e3e4d33579350d1b12001ef2872f86b27ed3dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/f2e3e4d33579350d1b12001ef2872f86b27ed3dc", + "reference": "f2e3e4d33579350d1b12001ef2872f86b27ed3dc", "shasum": "" }, "require": { @@ -6814,7 +6816,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.0.13" }, "funding": [ { @@ -6834,7 +6836,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-23T18:05:53+00:00" }, { "name": "symfony/type-info", @@ -7078,16 +7080,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.10", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c660d6538545a3e8e65a5621ee3d7a6d352892c7" + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c660d6538545a3e8e65a5621ee3d7a6d352892c7", - "reference": "c660d6538545a3e8e65a5621ee3d7a6d352892c7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", "shasum": "" }, "require": { @@ -7130,7 +7132,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.10" + "source": "https://github.com/symfony/yaml/tree/v7.4.13" }, "funding": [ { @@ -7150,7 +7152,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T08:01:55+00:00" + "time": "2026-05-25T06:06:12+00:00" }, { "name": "typo3/class-alias-loader", @@ -7216,23 +7218,23 @@ }, { "name": "typo3/cms-backend", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/backend.git", - "reference": "7b588ab6ee665bb96efe9eae18022ac9adeae86d" + "reference": "9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/7b588ab6ee665bb96efe9eae18022ac9adeae86d", - "reference": "7b588ab6ee665bb96efe9eae18022ac9adeae86d", + "url": "https://api.github.com/repos/TYPO3-CMS/backend/zipball/9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1", + "reference": "9b1278bd0a5c04d85090d96c8354b0fb5bcd89a1", "shasum": "" }, "require": { "ext-intl": "*", "ext-libxml": "*", "psr/event-dispatcher": "^1.0", - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7294,24 +7296,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-belog", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/belog.git", - "reference": "ef1755c9cb15c618fc739978378482d6599d8d07" + "reference": "e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/ef1755c9cb15c618fc739978378482d6599d8d07", - "reference": "ef1755c9cb15c618fc739978378482d6599d8d07", + "url": "https://api.github.com/repos/TYPO3-CMS/belog/zipball/e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55", + "reference": "e0cbf5d30b6cd9ff2430ede50668d8ab7b4cce55", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7352,24 +7354,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-beuser", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/beuser.git", - "reference": "d7cf3693c0988646bdd47bb55985e1fe6940c200" + "reference": "554788f60609f4b1ed61f46e584fb4277e931629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/d7cf3693c0988646bdd47bb55985e1fe6940c200", - "reference": "d7cf3693c0988646bdd47bb55985e1fe6940c200", + "url": "https://api.github.com/repos/TYPO3-CMS/beuser/zipball/554788f60609f4b1ed61f46e584fb4277e931629", + "reference": "554788f60609f4b1ed61f46e584fb4277e931629", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7410,7 +7412,7 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-cli", @@ -7519,16 +7521,16 @@ }, { "name": "typo3/cms-core", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/core.git", - "reference": "1d2c3cab3c4900cee5ba9af8e3e4885dfd6379a4" + "reference": "7779fe79725633fdafb18acb35b40c047acc1c0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/1d2c3cab3c4900cee5ba9af8e3e4885dfd6379a4", - "reference": "1d2c3cab3c4900cee5ba9af8e3e4885dfd6379a4", + "url": "https://api.github.com/repos/TYPO3-CMS/core/zipball/7779fe79725633fdafb18acb35b40c047acc1c0f", + "reference": "7779fe79725633fdafb18acb35b40c047acc1c0f", "shasum": "" }, "require": { @@ -7574,21 +7576,21 @@ "symfony/filesystem": "^7.2", "symfony/finder": "^7.2", "symfony/http-foundation": "^7.2", - "symfony/mailer": "^7.2", + "symfony/mailer": "^7.4.12", "symfony/messenger": "^7.2", - "symfony/mime": "^7.2", + "symfony/mime": "^7.4.12", "symfony/options-resolver": "^7.2", "symfony/polyfill-php83": "^1.31", "symfony/process": "^7.2", "symfony/rate-limiter": "^7.2", - "symfony/routing": "^7.2", + "symfony/routing": "^7.4.12", "symfony/uid": "^7.2", - "symfony/yaml": "^7.2", + "symfony/yaml": "^7.4.12", "typo3/class-alias-loader": "^1.2", "typo3/cms-cli": "^3.1.1", "typo3/cms-composer-installers": "^5.0.2", "typo3/html-sanitizer": "^2.2.0", - "typo3fluid/fluid": "^4.6.0" + "typo3fluid/fluid": "^4.6.1" }, "conflict": { "hoa/core": "*", @@ -7660,28 +7662,28 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-dashboard", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/dashboard.git", - "reference": "034bae46c060cc1d43994bf5ecdfc58e91679e8f" + "reference": "80aca12c9743de9d1a53747fc16bef1b9351d643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/034bae46c060cc1d43994bf5ecdfc58e91679e8f", - "reference": "034bae46c060cc1d43994bf5ecdfc58e91679e8f", + "url": "https://api.github.com/repos/TYPO3-CMS/dashboard/zipball/80aca12c9743de9d1a53747fc16bef1b9351d643", + "reference": "80aca12c9743de9d1a53747fc16bef1b9351d643", "shasum": "" }, "require": { - "typo3/cms-backend": "13.4.28", - "typo3/cms-core": "13.4.28", - "typo3/cms-extbase": "13.4.28", - "typo3/cms-fluid": "13.4.28", - "typo3/cms-frontend": "13.4.28" + "typo3/cms-backend": "13.4.30", + "typo3/cms-core": "13.4.30", + "typo3/cms-extbase": "13.4.30", + "typo3/cms-fluid": "13.4.30", + "typo3/cms-frontend": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7723,20 +7725,20 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-extbase", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extbase.git", - "reference": "e459fc97381bf884297fcbae8b22b5fef52f8de7" + "reference": "90f6fe6ecef724e42ade59decf6ff969d280a15e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/e459fc97381bf884297fcbae8b22b5fef52f8de7", - "reference": "e459fc97381bf884297fcbae8b22b5fef52f8de7", + "url": "https://api.github.com/repos/TYPO3-CMS/extbase/zipball/90f6fe6ecef724e42ade59decf6ff969d280a15e", + "reference": "90f6fe6ecef724e42ade59decf6ff969d280a15e", "shasum": "" }, "require": { @@ -7746,7 +7748,7 @@ "symfony/dependency-injection": "^7.2", "symfony/property-access": "^7.2", "symfony/property-info": "^7.2", - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7793,25 +7795,25 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-extensionmanager", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/extensionmanager.git", - "reference": "33691a16a90aa7bd3f92ccff58b8834efac39cd2" + "reference": "545ac7306bd6d8ee8fb520fc0107c78981474d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/33691a16a90aa7bd3f92ccff58b8834efac39cd2", - "reference": "33691a16a90aa7bd3f92ccff58b8834efac39cd2", + "url": "https://api.github.com/repos/TYPO3-CMS/extensionmanager/zipball/545ac7306bd6d8ee8fb520fc0107c78981474d90", + "reference": "545ac7306bd6d8ee8fb520fc0107c78981474d90", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7859,24 +7861,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-felogin", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/felogin.git", - "reference": "983083bf3aec242e969a2f2098ebce3938fb7413" + "reference": "0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/983083bf3aec242e969a2f2098ebce3938fb7413", - "reference": "983083bf3aec242e969a2f2098ebce3938fb7413", + "url": "https://api.github.com/repos/TYPO3-CMS/felogin/zipball/0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d", + "reference": "0de6d82b75589e0e0b2f6c38a827bfc3bbbd805d", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7917,24 +7919,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-filelist", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/filelist.git", - "reference": "3e61382a777806609a020f4ea896d89a2b1a0616" + "reference": "9f16745597dd84831855d1ef04ed6c07769ae8fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/3e61382a777806609a020f4ea896d89a2b1a0616", - "reference": "3e61382a777806609a020f4ea896d89a2b1a0616", + "url": "https://api.github.com/repos/TYPO3-CMS/filelist/zipball/9f16745597dd84831855d1ef04ed6c07769ae8fb", + "reference": "9f16745597dd84831855d1ef04ed6c07769ae8fb", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -7977,27 +7979,27 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-fluid", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid.git", - "reference": "dcbe6fb1dd1a8b6c147f0fcb36687c8e481fd30e" + "reference": "9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/dcbe6fb1dd1a8b6c147f0fcb36687c8e481fd30e", - "reference": "dcbe6fb1dd1a8b6c147f0fcb36687c8e481fd30e", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid/zipball/9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b", + "reference": "9b91fe7bc91351caef6a3d2905e05c96d9ee6e1b", "shasum": "" }, "require": { "symfony/dependency-injection": "^7.2", - "typo3/cms-core": "13.4.28", - "typo3/cms-extbase": "13.4.28", - "typo3fluid/fluid": "^4.6.0" + "typo3/cms-core": "13.4.30", + "typo3/cms-extbase": "13.4.30", + "typo3fluid/fluid": "^4.6.1" }, "conflict": { "typo3/cms": "*" @@ -8041,26 +8043,26 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-fluid-styled-content", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/fluid_styled_content.git", - "reference": "1728b33d9f8b6e380b8da6ea52c6325f2773b7f9" + "reference": "0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/1728b33d9f8b6e380b8da6ea52c6325f2773b7f9", - "reference": "1728b33d9f8b6e380b8da6ea52c6325f2773b7f9", + "url": "https://api.github.com/repos/TYPO3-CMS/fluid_styled_content/zipball/0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c", + "reference": "0d1eb82ef42b447a48099d6e37ccf580e8c2cb8c", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28", - "typo3/cms-fluid": "13.4.28", - "typo3/cms-frontend": "13.4.28" + "typo3/cms-core": "13.4.30", + "typo3/cms-fluid": "13.4.30", + "typo3/cms-frontend": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8101,27 +8103,27 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-form", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/form.git", - "reference": "85daca9bce24ce135962532189a9604ebea31248" + "reference": "fbe21be780125a642a265c6e25a76a7551de05d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/85daca9bce24ce135962532189a9604ebea31248", - "reference": "85daca9bce24ce135962532189a9604ebea31248", + "url": "https://api.github.com/repos/TYPO3-CMS/form/zipball/fbe21be780125a642a265c6e25a76a7551de05d7", + "reference": "fbe21be780125a642a265c6e25a76a7551de05d7", "shasum": "" }, "require": { "psr/http-message": "^1.1 || ^2.0", "symfony/expression-language": "^7.2", - "typo3/cms-core": "13.4.28", - "typo3/cms-frontend": "13.4.28" + "typo3/cms-core": "13.4.30", + "typo3/cms-frontend": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8167,25 +8169,25 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-frontend", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/frontend.git", - "reference": "32c07f2f21c36662d3bc4e436f5e3616af2730f4" + "reference": "d7588d8c2c426085b3b50338765b883dc8c77675" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/32c07f2f21c36662d3bc4e436f5e3616af2730f4", - "reference": "32c07f2f21c36662d3bc4e436f5e3616af2730f4", + "url": "https://api.github.com/repos/TYPO3-CMS/frontend/zipball/d7588d8c2c426085b3b50338765b883dc8c77675", + "reference": "d7588d8c2c426085b3b50338765b883dc8c77675", "shasum": "" }, "require": { "ext-libxml": "*", - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8237,24 +8239,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-impexp", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/impexp.git", - "reference": "c17f6d24cc7bb9a6cd21859de15737c05ee8530f" + "reference": "8a7fa24c720731783a0847a2f8250cfa07564898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/c17f6d24cc7bb9a6cd21859de15737c05ee8530f", - "reference": "c17f6d24cc7bb9a6cd21859de15737c05ee8530f", + "url": "https://api.github.com/repos/TYPO3-CMS/impexp/zipball/8a7fa24c720731783a0847a2f8250cfa07564898", + "reference": "8a7fa24c720731783a0847a2f8250cfa07564898", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8295,24 +8297,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-info", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/info.git", - "reference": "2fd1677a43311fadbc03db9950e681bb7a7c7f64" + "reference": "99a3a5b321b8b7db90de03ea1039217d1628a801" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/2fd1677a43311fadbc03db9950e681bb7a7c7f64", - "reference": "2fd1677a43311fadbc03db9950e681bb7a7c7f64", + "url": "https://api.github.com/repos/TYPO3-CMS/info/zipball/99a3a5b321b8b7db90de03ea1039217d1628a801", + "reference": "99a3a5b321b8b7db90de03ea1039217d1628a801", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8356,20 +8358,20 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-install", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/install.git", - "reference": "ae99f438c5814e51fad12fbced823b8a8f02f846" + "reference": "b5faf8708fa5322777104bbb9b31376882619cc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/ae99f438c5814e51fad12fbced823b8a8f02f846", - "reference": "ae99f438c5814e51fad12fbced823b8a8f02f846", + "url": "https://api.github.com/repos/TYPO3-CMS/install/zipball/b5faf8708fa5322777104bbb9b31376882619cc3", + "reference": "b5faf8708fa5322777104bbb9b31376882619cc3", "shasum": "" }, "require": { @@ -8378,9 +8380,9 @@ "nikic/php-parser": "^5.4.0", "symfony/finder": "^7.2", "symfony/http-foundation": "^7.2", - "typo3/cms-core": "13.4.28", - "typo3/cms-extbase": "13.4.28", - "typo3/cms-fluid": "13.4.28" + "typo3/cms-core": "13.4.30", + "typo3/cms-extbase": "13.4.30", + "typo3/cms-fluid": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8424,24 +8426,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-reactions", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/reactions.git", - "reference": "b5900e9327cc091ae94bad76c7ac80629eda8713" + "reference": "691910e92e9f57ad51a27acb587603ba2224c3ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/b5900e9327cc091ae94bad76c7ac80629eda8713", - "reference": "b5900e9327cc091ae94bad76c7ac80629eda8713", + "url": "https://api.github.com/repos/TYPO3-CMS/reactions/zipball/691910e92e9f57ad51a27acb587603ba2224c3ef", + "reference": "691910e92e9f57ad51a27acb587603ba2224c3ef", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8482,24 +8484,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-recycler", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/recycler.git", - "reference": "e9393eafc963541b2609a066f5fc156c7fb08491" + "reference": "9768124ed8d1961de0170e60085a10f8d8b4f51e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/e9393eafc963541b2609a066f5fc156c7fb08491", - "reference": "e9393eafc963541b2609a066f5fc156c7fb08491", + "url": "https://api.github.com/repos/TYPO3-CMS/recycler/zipball/9768124ed8d1961de0170e60085a10f8d8b4f51e", + "reference": "9768124ed8d1961de0170e60085a10f8d8b4f51e", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8543,24 +8545,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-rte-ckeditor", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/rte_ckeditor.git", - "reference": "18a62de15255070ab48dad11230b72983c07bd5d" + "reference": "f0b63855ebebe990067c79d0d0465aacd88556aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/18a62de15255070ab48dad11230b72983c07bd5d", - "reference": "18a62de15255070ab48dad11230b72983c07bd5d", + "url": "https://api.github.com/repos/TYPO3-CMS/rte_ckeditor/zipball/f0b63855ebebe990067c79d0d0465aacd88556aa", + "reference": "f0b63855ebebe990067c79d0d0465aacd88556aa", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8604,24 +8606,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-scheduler", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/scheduler.git", - "reference": "ad069c715c4b36ba31e05373750939e2145326aa" + "reference": "0a6688a0d52660250baae6c216c838c2d101cb66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/ad069c715c4b36ba31e05373750939e2145326aa", - "reference": "ad069c715c4b36ba31e05373750939e2145326aa", + "url": "https://api.github.com/repos/TYPO3-CMS/scheduler/zipball/0a6688a0d52660250baae6c216c838c2d101cb66", + "reference": "0a6688a0d52660250baae6c216c838c2d101cb66", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8659,26 +8661,26 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-seo", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/seo.git", - "reference": "0665e9230f7763fc2925d4490ad01c78e89e2dc1" + "reference": "fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/0665e9230f7763fc2925d4490ad01c78e89e2dc1", - "reference": "0665e9230f7763fc2925d4490ad01c78e89e2dc1", + "url": "https://api.github.com/repos/TYPO3-CMS/seo/zipball/fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62", + "reference": "fd3747d9b0133a1fcf737bd71282ce0e6fbf9f62", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28", - "typo3/cms-extbase": "13.4.28", - "typo3/cms-frontend": "13.4.28" + "typo3/cms-core": "13.4.30", + "typo3/cms-extbase": "13.4.30", + "typo3/cms-frontend": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8722,24 +8724,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-setup", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/setup.git", - "reference": "7e99c636b0f48321370cc2baacf5bccd346e0399" + "reference": "ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/setup/zipball/7e99c636b0f48321370cc2baacf5bccd346e0399", - "reference": "7e99c636b0f48321370cc2baacf5bccd346e0399", + "url": "https://api.github.com/repos/TYPO3-CMS/setup/zipball/ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f", + "reference": "ff6c5de7b5a2ef7cb3ea0f47f5b21ca3a662019f", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8780,24 +8782,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-sys-note", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/sys_note.git", - "reference": "3b79f7af8714eef69f192e64102241f523b4b439" + "reference": "b1d87efc54ffb9df6a9c5d24cd83d9833a730d32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/3b79f7af8714eef69f192e64102241f523b4b439", - "reference": "3b79f7af8714eef69f192e64102241f523b4b439", + "url": "https://api.github.com/repos/TYPO3-CMS/sys_note/zipball/b1d87efc54ffb9df6a9c5d24cd83d9833a730d32", + "reference": "b1d87efc54ffb9df6a9c5d24cd83d9833a730d32", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8841,24 +8843,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-tstemplate", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/tstemplate.git", - "reference": "7ba9eee7a88b3268de3b806dc044d3cffa7cdf67" + "reference": "76f71c2a8468df1ec0c78e1cf936af36c2d64123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/7ba9eee7a88b3268de3b806dc044d3cffa7cdf67", - "reference": "7ba9eee7a88b3268de3b806dc044d3cffa7cdf67", + "url": "https://api.github.com/repos/TYPO3-CMS/tstemplate/zipball/76f71c2a8468df1ec0c78e1cf936af36c2d64123", + "reference": "76f71c2a8468df1ec0c78e1cf936af36c2d64123", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8899,24 +8901,24 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-viewpage", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/viewpage.git", - "reference": "abfe3fbdca5f6a772242a2663438aa07bbb17fdf" + "reference": "826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/abfe3fbdca5f6a772242a2663438aa07bbb17fdf", - "reference": "abfe3fbdca5f6a772242a2663438aa07bbb17fdf", + "url": "https://api.github.com/repos/TYPO3-CMS/viewpage/zipball/826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0", + "reference": "826b9a3dee18444d8bc6d47b7a26e9e6720a7ef0", "shasum": "" }, "require": { - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -8957,25 +8959,25 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/cms-webhooks", - "version": "v13.4.28", + "version": "v13.4.30", "source": { "type": "git", "url": "https://github.com/TYPO3-CMS/webhooks.git", - "reference": "354b250de3658713ba7446072f6b1c9cc8ef5df7" + "reference": "381a808de044da905cf20245ec652536dc5c2d27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/354b250de3658713ba7446072f6b1c9cc8ef5df7", - "reference": "354b250de3658713ba7446072f6b1c9cc8ef5df7", + "url": "https://api.github.com/repos/TYPO3-CMS/webhooks/zipball/381a808de044da905cf20245ec652536dc5c2d27", + "reference": "381a808de044da905cf20245ec652536dc5c2d27", "shasum": "" }, "require": { "symfony/uid": "^7.2", - "typo3/cms-core": "13.4.28" + "typo3/cms-core": "13.4.30" }, "conflict": { "typo3/cms": "*" @@ -9016,7 +9018,7 @@ "issues": "https://forge.typo3.org", "source": "https://github.com/typo3/typo3" }, - "time": "2026-04-14T10:53:41+00:00" + "time": "2026-05-26T09:34:34+00:00" }, { "name": "typo3/html-sanitizer", @@ -9178,16 +9180,16 @@ }, { "name": "webmozart/assert", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", "shasum": "" }, "require": { @@ -9203,7 +9205,11 @@ }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { + "dev-master": "2.0-dev", "dev-feature/2-0": "2.0-dev" } }, @@ -9234,9 +9240,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.3.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.0" }, - "time": "2026-04-11T10:33:05+00:00" + "time": "2026-05-20T13:07:01+00:00" } ], "packages-dev": [], diff --git a/packages/vitec/Classes/Controller/OgImageController.php b/packages/vitec/Classes/Controller/OgImageController.php new file mode 100644 index 0000000..29a5068 --- /dev/null +++ b/packages/vitec/Classes/Controller/OgImageController.php @@ -0,0 +1,152 @@ +pageRenderer->addJsFile( + 'EXT:vitec/Resources/Public/Javascript/og-image-preview.js' + ); + + $moduleTemplate = $this->moduleTemplateFactory->create($request); + + $moduleTemplate->assignMultiple([ + 'formData' => $this->getDefaultFormData(), + 'savedImages' => $this->ogImageGeneratorService->getSavedImages(), + 'presets' => $this->getPresets(), + 'backgroundImages' => $this->ogImageGeneratorService->getBackgroundImages(), + ]); + + return $moduleTemplate->renderResponse('OgImage/Index'); + } + + /** + * Handles form submission and generates the OG image. + */ + public function generateAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $formData = $parsedBody['ogimage'] ?? []; + + [$success, $message, $filePath] = $this->ogImageGeneratorService->generate($formData); + + $severity = $success + ? ContextualFeedbackSeverity::OK + : ContextualFeedbackSeverity::ERROR; + + $flashMessage = GeneralUtility::makeInstance( + FlashMessage::class, + $message, + $success ? 'OG Image created' : 'Generation failed', + $severity, + true + ); + $this->flashMessageService + ->getMessageQueueByIdentifier() + ->addMessage($flashMessage); + + $moduleUrl = (string)$this->uriBuilder->buildUriFromRoute('web_vitecogimage'); + return new RedirectResponse($moduleUrl, 303); + } + + /** + * Returns a base64 PNG preview without saving to disk (AJAX). + */ + public function previewAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $formData = $parsedBody['ogimage'] ?? []; + + [$success, $message, $dataUri] = $this->ogImageGeneratorService->generatePreview($formData); + + return new JsonResponse([ + 'success' => $success, + 'message' => $message, + 'dataUri' => $dataUri, + ]); + } + + // ────────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────────── + + private function getDefaultFormData(): array + { + return [ + 'bg_type' => 'color', + 'bg_color' => '#1a1a2e', + 'bg_image' => '', + 'overlay_opacity' => 40, + 'title' => 'Your Page Title', + 'title_color' => '#ffffff', + 'title_size' => 64, + 'subtitle' => 'A short description of your page', + 'subtitle_color' => '#cccccc', + 'subtitle_size' => 32, + 'label_text' => 'VITEC', + 'label_bg_color' => '#e63946', + 'label_text_color' => '#ffffff', + 'label_position' => 'top-left', + 'logo_path' => '', + 'output_format' => 'jpg', + 'output_quality' => 90, + ]; + } + + private function getPresets(): array + { + return [ + 'dark-blue' => [ + 'label' => 'Dark Blue', + 'bg_color' => '#1a1a2e', + 'title_color' => '#ffffff', + 'subtitle_color' => '#aaaacc', + ], + 'light' => [ + 'label' => 'Clean Light', + 'bg_color' => '#f8f9fa', + 'title_color' => '#212529', + 'subtitle_color' => '#6c757d', + ], + 'brand-red' => [ + 'label' => 'VITEC Brand Red', + 'bg_color' => '#1f1f1f', + 'title_color' => '#ffffff', + 'subtitle_color' => '#e63946', + ], + ]; + } +} diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php index 7bc36d7..cbb8eb0 100755 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -3,6 +3,10 @@ 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 TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -15,6 +19,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 +36,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 +46,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 +59,19 @@ 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'], + ]; + public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, @@ -124,6 +143,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 +161,48 @@ 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 $record + * @param array $data + */ + private 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; + + // 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 diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 new file mode 100755 index 0000000..7bc36d7 --- /dev/null +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php.bak.20260518160048 @@ -0,0 +1,157 @@ +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); + } + + 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, + ]; + } + + 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; + } +} diff --git a/packages/vitec/Classes/Service/OgImageGeneratorService.php b/packages/vitec/Classes/Service/OgImageGeneratorService.php new file mode 100644 index 0000000..cb931c3 --- /dev/null +++ b/packages/vitec/Classes/Service/OgImageGeneratorService.php @@ -0,0 +1,419 @@ +buildImage($opts); + + $saveDir = Environment::getPublicPath() . '/' . self::SAVE_DIR; + GeneralUtility::mkdir_deep($saveDir); + + $hashSeed = json_encode($opts, JSON_THROW_ON_ERROR) . microtime(true) . random_int(1000, 9999); + $fileName = 'og-' . substr(sha1($hashSeed), 0, 12) . '.' . ($opts['output_format'] ?? 'jpg'); + $fullPath = $saveDir . $fileName; + + $this->saveImage($gd, $fullPath, $opts['output_format'] ?? 'jpg', (int)($opts['output_quality'] ?? 90)); + imagedestroy($gd); + + $publicUrl = '/' . self::SAVE_DIR . $fileName; + + return [true, 'Image saved: ' . $publicUrl, $publicUrl]; + } catch (\Throwable $e) { + return [false, 'Error: ' . $e->getMessage(), '']; + } + } + + /** + * Generate a base64 data-URI preview (no file write). + * + * @return array{0: bool, 1: string, 2: string} [success, message, dataUri] + */ + public function generatePreview(array $opts): array + { + try { + $gd = $this->buildImage($opts); + + ob_start(); + imagepng($gd); + $raw = ob_get_clean(); + imagedestroy($gd); + + return [true, 'OK', 'data:image/png;base64,' . base64_encode($raw)]; + } catch (\Throwable $e) { + return [false, $e->getMessage(), '']; + } + } + + /** + * Return list of previously generated images in the save dir. + */ + public function getSavedImages(): array + { + $dir = Environment::getPublicPath() . '/' . self::SAVE_DIR; + if (!is_dir($dir)) { + return []; + } + + $files = glob($dir . '*.{jpg,jpeg,png}', GLOB_BRACE) ?: []; + usort($files, static fn($a, $b) => filemtime($b) <=> filemtime($a)); + + return array_map(static fn($f) => [ + 'path' => '/' . self::SAVE_DIR . basename($f), + 'name' => basename($f), + 'created' => date('Y-m-d H:i', filemtime($f)), + ], array_slice($files, 0, 20)); + } + + /** + * Return selectable background images from fileadmin/og-backgrounds/. + */ + public function getBackgroundImages(): array + { + $dir = Environment::getPublicPath() . '/fileadmin/og-backgrounds/'; + if (!is_dir($dir)) { + return []; + } + + $files = glob($dir . '*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: []; + + return array_map(static fn($f) => [ + 'path' => '/fileadmin/og-backgrounds/' . basename($f), + 'label' => basename($f), + ], $files); + } + + // ────────────────────────────────────────────────────────────────── + // Core image builder + // ────────────────────────────────────────────────────────────────── + + /** @return \GdImage */ + private function buildImage(array $opts): \GdImage + { + $canvas = imagecreatetruecolor(self::OG_WIDTH, self::OG_HEIGHT); + if ($canvas === false) { + throw new \RuntimeException('imagecreatetruecolor failed – GD not available.'); + } + + // Enable alpha blending + imagealphablending($canvas, true); + imagesavealpha($canvas, true); + + // 1. Background + $this->drawBackground($canvas, $opts); + + // 2. Optional dark overlay when bg_image is set + if (!empty($opts['bg_image']) && ($opts['bg_type'] ?? '') === 'image') { + $opacity = max(0, min(100, (int)($opts['overlay_opacity'] ?? 40))); + $this->drawOverlay($canvas, $opacity); + } + + // 3. Label / badge + if (!empty(trim($opts['label_text'] ?? ''))) { + $this->drawLabel($canvas, $opts); + } + + // 4. Title + if (!empty(trim($opts['title'] ?? ''))) { + $this->drawTitle($canvas, $opts); + } + + // 5. Subtitle + if (!empty(trim($opts['subtitle'] ?? ''))) { + $this->drawSubtitle($canvas, $opts); + } + + // 6. Logo (optional) + if (!empty($opts['logo_path'])) { + $this->drawLogo($canvas, $opts['logo_path']); + } + + return $canvas; + } + + // ────────────────────────────────────────────────────────────────── + // Drawing helpers + // ────────────────────────────────────────────────────────────────── + + private function drawBackground(\GdImage $canvas, array $opts): void + { + $bgType = $opts['bg_type'] ?? 'color'; + + if ($bgType === 'image' && !empty($opts['bg_image'])) { + $imagePath = $this->resolvePath($opts['bg_image']); + $src = $this->loadImageFromPath($imagePath); + + if ($src !== null) { + imagecopyresampled( + $canvas, $src, + 0, 0, 0, 0, + self::OG_WIDTH, self::OG_HEIGHT, + imagesx($src), imagesy($src) + ); + imagedestroy($src); + return; + } + } + + // Solid colour fallback + $hex = ltrim($opts['bg_color'] ?? '#1a1a2e', '#'); + [$r, $g, $b] = $this->hexToRgb($hex); + $bg = imagecolorallocate($canvas, $r, $g, $b); + imagefilledrectangle($canvas, 0, 0, self::OG_WIDTH - 1, self::OG_HEIGHT - 1, $bg); + } + + private function drawOverlay(\GdImage $canvas, int $opacityPercent): void + { + // opacity 0 = fully transparent overlay, 100 = solid black + $alpha = (int)round(127 - ($opacityPercent / 100 * 127)); + $color = imagecolorallocatealpha($canvas, 0, 0, 0, $alpha); + imagefilledrectangle($canvas, 0, 0, self::OG_WIDTH - 1, self::OG_HEIGHT - 1, $color); + } + + private function drawLabel(\GdImage $canvas, array $opts): void + { + $text = strtoupper(trim($opts['label_text'] ?? 'LABEL')); + $fontSize = 22; + $font = $this->fontPath(true); + + $padding = 14; + $bbox = imagettfbbox($fontSize, 0, $font, $text); + $textW = abs($bbox[4] - $bbox[0]); + $textH = abs($bbox[5] - $bbox[1]); + $boxW = $textW + $padding * 2; + $boxH = $textH + $padding; + + $position = $opts['label_position'] ?? 'top-left'; + [$bx, $by] = $this->labelCoords($position, $boxW, $boxH); + + [$br, $bg, $bb] = $this->hexToRgb(ltrim($opts['label_bg_color'] ?? '#e63946', '#')); + $bgColor = imagecolorallocate($canvas, $br, $bg, $bb); + imagefilledrectangle($canvas, $bx, $by, $bx + $boxW, $by + $boxH, $bgColor); + + [$tr, $tg, $tb] = $this->hexToRgb(ltrim($opts['label_text_color'] ?? '#ffffff', '#')); + $textColor = imagecolorallocate($canvas, $tr, $tg, $tb); + imagettftext($canvas, $fontSize, 0, $bx + $padding, $by + $textH + (int)($padding / 2), $textColor, $font, $text); + } + + private function drawTitle(\GdImage $canvas, array $opts): void + { + $text = trim($opts['title'] ?? ''); + $fontSize = max(20, min(120, (int)($opts['title_size'] ?? 64))); + $font = $this->fontPath(true); + $maxWidth = self::OG_WIDTH - self::PADDING * 2; + + [$r, $g, $b] = $this->hexToRgb(ltrim($opts['title_color'] ?? '#ffffff', '#')); + $color = imagecolorallocate($canvas, $r, $g, $b); + + $lines = $this->wrapText($text, $fontSize, $font, $maxWidth); + $lineH = (int)($fontSize * 1.3); + $startY = $this->titleStartY($opts, count($lines), $lineH, $fontSize); + + foreach ($lines as $i => $line) { + $y = $startY + $i * $lineH; + imagettftext($canvas, $fontSize, 0, self::PADDING, $y, $color, $font, $line); + } + } + + private function drawSubtitle(\GdImage $canvas, array $opts): void + { + $text = trim($opts['subtitle'] ?? ''); + $fontSize = max(14, min(80, (int)($opts['subtitle_size'] ?? 32))); + $font = $this->fontPath(false); + $maxWidth = self::OG_WIDTH - self::PADDING * 2; + + [$r, $g, $b] = $this->hexToRgb(ltrim($opts['subtitle_color'] ?? '#cccccc', '#')); + $color = imagecolorallocate($canvas, $r, $g, $b); + + $lines = $this->wrapText($text, $fontSize, $font, $maxWidth); + $lineH = (int)($fontSize * 1.4); + $startY = $this->subtitleStartY($opts, $fontSize); + + foreach ($lines as $i => $line) { + $y = $startY + $i * $lineH; + imagettftext($canvas, $fontSize, 0, self::PADDING, $y, $color, $font, $line); + } + } + + private function drawLogo(\GdImage $canvas, string $logoPath): void + { + $path = $this->resolvePath($logoPath); + $src = $this->loadImageFromPath($path); + if ($src === null) { + return; + } + + $logoW = 200; + $logoH = (int)(imagesy($src) * ($logoW / imagesx($src))); + $x = self::OG_WIDTH - self::PADDING - $logoW; + $y = self::OG_HEIGHT - self::PADDING - $logoH; + + imagecopyresampled($canvas, $src, $x, $y, 0, 0, $logoW, $logoH, imagesx($src), imagesy($src)); + imagedestroy($src); + } + + // ────────────────────────────────────────────────────────────────── + // Layout helpers + // ────────────────────────────────────────────────────────────────── + + private function titleStartY(array $opts, int $lineCount, int $lineH, int $fontSize): int + { + $totalH = $lineCount * $lineH; + $subH = empty(trim($opts['subtitle'] ?? '')) ? 0 : (int)($opts['subtitle_size'] ?? 32) + 20; + $block = $totalH + $subH; + $center = (int)((self::OG_HEIGHT - $block) / 2); + // nudge up slightly so text block feels centered + return max(self::PADDING + $fontSize, $center); + } + + private function subtitleStartY(array $opts, int $fontSize): int + { + $titleSize = max(20, min(120, (int)($opts['title_size'] ?? 64))); + $titleText = trim($opts['title'] ?? ''); + $font = $this->fontPath(true); + $maxWidth = self::OG_WIDTH - self::PADDING * 2; + $titleLines = $this->wrapText($titleText, $titleSize, $font, $maxWidth); + $titleLineH = (int)($titleSize * 1.3); + $titleBlock = count($titleLines) * $titleLineH; + + $subH = empty(trim($opts['subtitle'] ?? '')) ? 0 : $fontSize + 20; + $block = $titleBlock + $subH; + $center = (int)((self::OG_HEIGHT - $block) / 2); + $topOfTitle = max(self::PADDING + $titleSize, $center); + + return $topOfTitle + $titleBlock + 20; + } + + private function labelCoords(string $position, int $w, int $h): array + { + $pad = self::PADDING; + return match ($position) { + 'top-right' => [self::OG_WIDTH - $pad - $w, $pad], + 'bottom-left' => [$pad, self::OG_HEIGHT - $pad - $h], + 'bottom-right' => [self::OG_WIDTH - $pad - $w, self::OG_HEIGHT - $pad - $h], + default => [$pad, $pad], // top-left + }; + } + + // ────────────────────────────────────────────────────────────────── + // Utility helpers + // ────────────────────────────────────────────────────────────────── + + /** Wrap text to fit within $maxWidth px */ + private function wrapText(string $text, int $fontSize, string $font, int $maxWidth): array + { + $words = explode(' ', $text); + $lines = []; + $current = ''; + + foreach ($words as $word) { + $test = $current !== '' ? "$current $word" : $word; + $bbox = imagettfbbox($fontSize, 0, $font, $test); + $w = abs($bbox[4] - $bbox[0]); + + if ($w > $maxWidth && $current !== '') { + $lines[] = $current; + $current = $word; + } else { + $current = $test; + } + } + + if ($current !== '') { + $lines[] = $current; + } + + return $lines ?: ['']; + } + + private function fontPath(bool $bold): string + { + $path = $bold ? self::FONT_BOLD : self::FONT_REG; + if (!file_exists($path)) { + $path = self::FONT_REG; // fallback to regular + } + if (!file_exists($path)) { + throw new \RuntimeException("TTF font not found at $path"); + } + return $path; + } + + private function hexToRgb(string $hex): array + { + $hex = ltrim($hex, '#'); + if (strlen($hex) === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + return [ + hexdec(substr($hex, 0, 2)), + hexdec(substr($hex, 2, 2)), + hexdec(substr($hex, 4, 2)), + ]; + } + + private function resolvePath(string $path): string + { + if (str_starts_with($path, '/')) { + return Environment::getPublicPath() . $path; + } + return Environment::getPublicPath() . '/' . ltrim($path, '/'); + } + + /** @return \GdImage|null */ + private function loadImageFromPath(string $path): ?\GdImage + { + if (!file_exists($path)) { + return null; + } + + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + + return match ($ext) { + 'jpg', 'jpeg' => imagecreatefromjpeg($path) ?: null, + 'png' => imagecreatefrompng($path) ?: null, + 'gif' => imagecreatefromgif($path) ?: null, + 'webp' => imagecreatefromwebp($path) ?: null, + default => null, + }; + } + + private function saveImage(\GdImage $gd, string $path, string $format, int $quality): void + { + match (strtolower($format)) { + 'png' => imagepng($gd, $path, max(0, min(9, (int)(9 - $quality / 11)))), + default => imagejpeg($gd, $path, $quality), + }; + } +} diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index a65264d..fd48d4e 100644 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -16,23 +16,19 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Service\ImageService; /** - * UserFunc to render product list as JSON for headless + * 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 { - // 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'); @@ -49,95 +45,93 @@ class ProductListJsonRenderer ->fetchAllAssociative(); if (empty($contentElements)) { - return json_encode(['debug' => 'No vitec_productlist on page ' . $pageId]); + // Not a product-list page: emit nothing so headless removes the key. + return ''; } - // Take the first one (there should typically be only one) - $contentElement = $contentElements[0]; + return $this->renderForRecord($contentElements[0]); + } - // Parse FlexForm - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); - $settings = $flexFormData['settings'] ?? []; + /** + * Render exactly the given tt_content row (the product-list plugin element). + * Exception-safe: returns '' on any failure. + * + * @param array $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'] ?? []; - // 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) + $categoryUids = array_filter( + array_map('intval', explode(',', (string)($settings['categories'] ?? ''))) ); + $debugMode = (bool)($settings['debug'] ?? false); + $allProducts = (bool)($settings['allproducts'] ?? false); - // 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'); + // 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 ''; } - - $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 $product Associative DB row of tx_vitec_domain_model_product + * @param array $product * @return array */ protected function serializeProduct(array $product): array @@ -145,10 +139,8 @@ class ProductListJsonRenderer $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'] ?? ''), @@ -166,7 +158,6 @@ class ProductListJsonRenderer '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), @@ -176,10 +167,8 @@ class ProductListJsonRenderer '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), @@ -188,10 +177,6 @@ class ProductListJsonRenderer ]; } - /** - * 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) @@ -217,10 +202,8 @@ class ProductListJsonRenderer $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], @@ -247,7 +230,6 @@ class ProductListJsonRenderer ]; } - // Get original/default image $defaultProcessed = $imageService->applyProcessingInstructions( $fileReference, ['width' => 800, 'crop' => $fileRefData['crop'] ?? null] @@ -267,7 +249,6 @@ class ProductListJsonRenderer ] ]; } catch (\Exception $e) { - // Skip images that can't be processed continue; } } @@ -276,8 +257,6 @@ class ProductListJsonRenderer } /** - * Get the single Open Graph image (ogimage) for a product, or null. - * * @return array|null */ protected function getProductOgImage(int $productUid): ?array @@ -331,9 +310,6 @@ class ProductListJsonRenderer } } - /** - * Get categories for a product (resolved sys_category records). - */ protected function getProductCategories(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) @@ -369,9 +345,6 @@ class ProductListJsonRenderer }, $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) @@ -400,7 +373,6 @@ class ProductListJsonRenderer 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'); @@ -448,12 +420,6 @@ class ProductListJsonRenderer 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) @@ -480,8 +446,6 @@ class ProductListJsonRenderer $result = []; foreach ($related as $rel) { $relUid = (int)$rel['uid']; - $images = $this->getProductImages($relUid); - $result[] = [ 'uid' => $relUid, 'title' => (string)($rel['title'] ?? ''), @@ -490,7 +454,7 @@ class ProductListJsonRenderer 'teaser' => (string)($rel['teaser'] ?? ''), 'description' => (string)($rel['description'] ?? ''), 'link' => '/product/' . (string)($rel['slug'] ?? ''), - 'images' => $images, + 'images' => $this->getProductImages($relUid), ]; } diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 new file mode 100755 index 0000000..a65264d --- /dev/null +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php.bak.20260518160048 @@ -0,0 +1,499 @@ +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 $product Associative DB row of tx_vitec_domain_model_product + * @return array + */ + 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|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; + } +} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php index 8bb69b3..a7ae85b 100644 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -14,25 +14,22 @@ use TYPO3\CMS\Core\Imaging\ImageService; use Doctrine\DBAL\ParameterType; /** - * UserFunc to render single product data as JSON for headless output + * 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; - - // 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 + $pageId = (int)($GLOBALS['TSFE']->id ?? 0); + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); - + $contentElements = $queryBuilder ->select('*') ->from('tt_content') @@ -44,140 +41,161 @@ class ProductShowJsonRenderer ) ->executeQuery() ->fetchAllAssociative(); - - $debugInfo['contentElementsFound'] = count($contentElements); - + if (empty($contentElements)) { - $debugInfo['error'] = 'No vitec_productshow on page'; - return json_encode(['debug' => $debugInfo]); + return ''; } - - // 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; + + 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 $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 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' => [ + + 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, - ], - ]; - - if ($debugMode) { - $response['debug'] = [ - 'pageId' => $pageId, - 'productUid' => $productUid, - 'layout' => $layout, - 'settings' => $settings, + 'settings' => [ + 'layout' => $layout, + ], ]; + + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, + 'productUid' => $productUid, + 'layout' => $layout, + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; } - - return json_encode($response); } - + /** - * Get all images for a product with FAL and ImageService processing + * @param array $product + * @return array */ + 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') @@ -191,27 +209,25 @@ class ProductShowJsonRenderer ->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( @@ -224,7 +240,7 @@ class ProductShowJsonRenderer 'descriptor' => $width . 'w', ]; } - + $images[] = [ 'uid' => $fileRef['uid'], 'url' => $imageService->getImageUri($processedImage), @@ -239,22 +255,73 @@ class ProductShowJsonRenderer ], ]; } catch (\Exception $e) { - // Skip invalid file references continue; } } - + return $images; } - + /** - * Get categories for a product + * @return array|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') @@ -262,9 +329,9 @@ class ProductShowJsonRenderer '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 = ' . + '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( @@ -275,7 +342,7 @@ class ProductShowJsonRenderer ->orderBy('mm.sorting', 'ASC') ->executeQuery() ->fetchAllAssociative(); - + return array_map(function ($cat) { return [ 'uid' => (int)$cat['uid'], @@ -284,15 +351,12 @@ class ProductShowJsonRenderer ]; }, $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') @@ -311,16 +375,15 @@ class ProductShowJsonRenderer ->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') @@ -336,7 +399,7 @@ class ProductShowJsonRenderer ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); - + if ($fileRef) { $fileInfo = [ 'uid' => (int)$fileRef['file_uid'], @@ -348,7 +411,7 @@ class ProductShowJsonRenderer ]; } } - + $result[] = [ 'uid' => (int)$download['uid'], 'title' => $download['title'] ?? '', @@ -360,7 +423,48 @@ class ProductShowJsonRenderer '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; } } diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 new file mode 100755 index 0000000..8bb69b3 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518145244 @@ -0,0 +1,366 @@ +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; + } +} diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 new file mode 100755 index 0000000..dda264f --- /dev/null +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php.bak.20260518160048 @@ -0,0 +1,512 @@ +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 $product Associative DB row of tx_vitec_domain_model_product + * @return array + */ + 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|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; + } +} diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php new file mode 100755 index 0000000..b3792e0 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php @@ -0,0 +1,223 @@ +id ?? 0); + + $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)) { + return ''; + } + + return $this->renderForRecord($contentElements[0]); + } + + /** + * @param array $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); + + $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' => (int)($GLOBALS['TSFE']->id ?? 0), + 'usecaseCount' => count($usecasesData), + 'settings' => $settings, + ], + ]); + } + + return json_encode($usecasesData); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @param array $usecase + * @return array + */ + protected function serializeUsecase(array $usecase): array + { + $uid = (int)$usecase['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'), + ]; + } + + /** + * @return array|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']); + + $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 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); + } +} diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 new file mode 100755 index 0000000..91b3d8d --- /dev/null +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php.bak.20260518160048 @@ -0,0 +1,244 @@ +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 $usecase + * @return array + */ + 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|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); + } +} diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php new file mode 100755 index 0000000..b57f391 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php @@ -0,0 +1,265 @@ +id ?? 0); + + $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 ''; + } + + return $this->renderForRecord($contentElements[0]); + } + + /** + * @param array $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'] ?? []; + + $usecaseUid = (int)($settings['usecase'] ?? 0); + $layout = (string)($settings['layout'] ?? 'default'); + $debugMode = (bool)($settings['debug'] ?? false); + + if (!$usecaseUid) { + $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; + } + } + } + + if (!$usecaseUid) { + return $debugMode + ? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]]) + : ''; + } + + $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); + + $usecase = $usecaseQb + ->select('*') + ->from('tx_vitec_domain_model_usecase') + ->where( + $usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $usecaseQb->expr()->eq('deleted', 0), + $usecaseQb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$usecase) { + return $debugMode + ? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]]) + : ''; + } + + $response = [ + 'usecase' => $this->serializeUsecase($usecase), + 'layout' => $layout, + 'settings' => [ + 'layout' => $layout, + ], + ]; + + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, + 'usecaseUid' => $usecaseUid, + 'layout' => $layout, + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * @param array $usecase + * @return array + */ + protected function serializeUsecase(array $usecase): array + { + $uid = (int)$usecase['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'), + ]; + } + + /** + * @return array|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']); + + $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 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); + } +} diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 new file mode 100755 index 0000000..e469c96 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php.bak.20260518160048 @@ -0,0 +1,271 @@ +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); + $layout = (string)($settings['layout'] ?? 'default'); + $debugMode = (bool)($settings['debug'] ?? false); + + // Fall back to route / query parameter (uid or slug) + if (!$usecaseUid) { + $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; + } + } + } + + if (!$usecaseUid) { + return $debugMode + ? json_encode(['error' => 'No usecase selected or found', 'debug' => ['settings' => $settings]]) + : ''; + } + + // Query the usecase + $usecaseQb = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_usecase'); + + $usecase = $usecaseQb + ->select('*') + ->from('tx_vitec_domain_model_usecase') + ->where( + $usecaseQb->expr()->eq('uid', $usecaseQb->createNamedParameter($usecaseUid, ParameterType::INTEGER)), + $usecaseQb->expr()->eq('deleted', 0), + $usecaseQb->expr()->eq('hidden', 0) + ) + ->executeQuery() + ->fetchAssociative(); + + if (!$usecase) { + return $debugMode + ? json_encode(['error' => 'Usecase not found', 'debug' => ['usecaseUid' => $usecaseUid]]) + : ''; + } + + $response = [ + 'usecase' => $this->serializeUsecase($usecase), + 'layout' => $layout, + 'settings' => [ + 'layout' => $layout, + ], + ]; + + if ($debugMode) { + $response['debug'] = [ + 'pageId' => $pageId, + 'usecaseUid' => $usecaseUid, + 'layout' => $layout, + 'settings' => $settings, + ]; + } + + return json_encode($response); + } catch (\Throwable $e) { + return ''; + } + } + + /** + * Serialize a single usecase DB row. Structurally identical to + * UsecaseListJsonRenderer::serializeUsecase() so list and detail + * endpoints expose the same usecase schema. + * + * @param array $usecase + * @return array + */ + protected function serializeUsecase(array $usecase): array + { + $uid = (int)$usecase['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'), + ]; + } + + /** + * Resolve a single FAL image field (caseimage / logoimage) with srcset. + * + * @return array|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']); + + $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; + } + } + + /** + * 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); + } +} diff --git a/packages/vitec/Configuration/Backend/Modules.php b/packages/vitec/Configuration/Backend/Modules.php new file mode 100644 index 0000000..445dd4f --- /dev/null +++ b/packages/vitec/Configuration/Backend/Modules.php @@ -0,0 +1,34 @@ + [ + 'parent' => 'web', + 'position' => ['after' => 'web_info'], + 'access' => 'user', + 'workspaces' => 'live', + 'path' => '/module/web/vitec-ogimage', + 'labels' => [ + 'title' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf:mlang_tabs_tab', + 'shortDescription' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf:mlang_labels_tabdescr', + ], + 'extensionName' => 'Vitec', + 'iconIdentifier' => 'vitec-ogimage', + 'routes' => [ + '_default' => [ + 'target' => OgImageController::class . '::indexAction', + ], + 'generate' => [ + 'target' => OgImageController::class . '::generateAction', + 'methods' => ['POST'], + ], + 'preview' => [ + 'target' => OgImageController::class . '::previewAction', + 'methods' => ['POST'], + ], + ], + ], +]; diff --git a/packages/vitec/Configuration/FlexForms/Container.xml b/packages/vitec/Configuration/FlexForms/Container.xml new file mode 100755 index 0000000..d5d6fa6 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Container.xml @@ -0,0 +1,22 @@ + + + + + + LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.sheet + array + + + + LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.cssclass.description + + input + 30 + trim + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Usecaselist.xml b/packages/vitec/Configuration/FlexForms/Usecaselist.xml index f7a1cb6..e8e7369 100644 --- a/packages/vitec/Configuration/FlexForms/Usecaselist.xml +++ b/packages/vitec/Configuration/FlexForms/Usecaselist.xml @@ -3,27 +3,22 @@ - - Select Usecase - + Usecase List array - - + + - select - selectSingle - tx_vitec_domain_model_usecase - AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title - 1 - 0 - 1 + check + + + + + - + - \ No newline at end of file + diff --git a/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 b/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 new file mode 100755 index 0000000..f7a1cb6 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Usecaselist.xml.bak.20260518153402 @@ -0,0 +1,29 @@ + + + + + + + Select Usecase + + array + + + + + select + selectSingle + tx_vitec_domain_model_usecase + AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 ORDER BY tx_vitec_domain_model_usecase.title + 1 + 0 + 1 + + + + + + + \ No newline at end of file diff --git a/packages/vitec/Configuration/Icons.php b/packages/vitec/Configuration/Icons.php index b6f157e..02328b4 100755 --- a/packages/vitec/Configuration/Icons.php +++ b/packages/vitec/Configuration/Icons.php @@ -24,4 +24,8 @@ return [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-cols-33-66.svg', ], + 'vitec-ogimage' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-ogimage.svg', + ], ]; diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript index 17b9f1a..f302731 100644 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript @@ -8,28 +8,82 @@ config { } } -# Headless: Add products field to vitec_productlist +# ============================================================================= +# Headless list-plugin JSON renderers +# +# `tt_content.list.20.` renders the element body for that subtype +# (only ever invoked for the matching list_type — safe as-is). +# +# `tt_content.list.fields.content.fields.` is GLOBAL: without scoping it +# would inject 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 } -# 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 + stdWrap.if { + value = vitec_productlist + equals.field = list_type + } + ifEmptyUnsetKey = 1 } -# Headless: Add product field to vitec_productshow +# --- 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 + 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 +} + +# --- 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 + } + ifEmptyUnsetKey = 1 } # Include container (layout) rendering definitions diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 new file mode 100755 index 0000000..17b9f1a --- /dev/null +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518152905 @@ -0,0 +1,44 @@ +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' diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 new file mode 100755 index 0000000..042963f --- /dev/null +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript.bak.20260518154740 @@ -0,0 +1,82 @@ +config { + pageTitleProviders { + vitec { + provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider + before = record + before = seo + } + } +} + +# ============================================================================= +# Headless list-plugin JSON renderers +# +# `tt_content.list.20.` renders the element body for that subtype +# (only ever invoked for the matching list_type — safe as-is). +# +# `tt_content.list.fields.content.fields.` is GLOBAL: without scoping it +# would inject 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' diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php new file mode 100755 index 0000000..e53607f --- /dev/null +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php @@ -0,0 +1,69 @@ +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;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.label, + --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'. + // The official helper registers the data structure under the key + // '*,vitec_container' (wildcard list_type + CType), which is the + // version-proof way to attach a FlexForm to a CType-based element. + ExtensionManagementUtility::addPiFlexFormValue( + '*', + 'FILE:EXT:vitec/Configuration/FlexForms/Container.xml', + 'vitec_container' + ); + + ExtensionManagementUtility::addPageTSConfig(<<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(<<Narrow sidebar + wide main column + + VITEC · Container + + + Generic container for other content elements with a custom CSS class + + + Content + + + Container Settings + + + Container Settings + + + CSS Class + + + Custom CSS class that is rendered on the container in the frontend. + + Column 1 diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 new file mode 100755 index 0000000..68279b8 --- /dev/null +++ b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518150609 @@ -0,0 +1,91 @@ + + + +
+ + + VITEC + + + + Section Heading + + + Section Subline + + + + Background Variant + + + None + + + Orange + + + Blue + + + Graphite + + + Midnight + + + + VITEC · Two Columns (50 / 50) + + + Two equal columns + + + + VITEC · Three Columns (33 / 33 / 33) + + + Three equal columns + + + + VITEC · Four Columns (25 / 25 / 25 / 25) + + + Four equal columns + + + + VITEC · Two Columns (66 / 33) + + + Wide main column + narrow sidebar + + + + VITEC · Two Columns (33 / 66) + + + Narrow sidebar + wide main column + + + + Column 1 + + + Column 2 + + + Column 3 + + + Column 4 + + + Main (66%) + + + Sidebar (33%) + + + + diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 new file mode 100755 index 0000000..cbd5a4d --- /dev/null +++ b/packages/vitec/Resources/Private/Language/locallang_containers.xlf.bak.20260518151017 @@ -0,0 +1,110 @@ + + + +
+ + + VITEC + + + + Section Heading + + + Section Subline + + + + Background Variant + + + None + + + Orange + + + Blue + + + Graphite + + + Midnight + + + + VITEC · Two Columns (50 / 50) + + + Two equal columns + + + + VITEC · Three Columns (33 / 33 / 33) + + + Three equal columns + + + + VITEC · Four Columns (25 / 25 / 25 / 25) + + + Four equal columns + + + + VITEC · Two Columns (66 / 33) + + + Wide main column + narrow sidebar + + + + VITEC · Two Columns (33 / 66) + + + Narrow sidebar + wide main column + + + + VITEC · Container + + + Generic container for other content elements with a custom CSS class + + + Content + + + Container Settings + + + CSS Class + + + Custom CSS class that is rendered on the container in the frontend. + + + + Column 1 + + + Column 2 + + + Column 3 + + + Column 4 + + + Main (66%) + + + Sidebar (33%) + + + + diff --git a/packages/vitec/Resources/Private/Language/locallang_ogimage.xlf b/packages/vitec/Resources/Private/Language/locallang_ogimage.xlf new file mode 100644 index 0000000..3707385 --- /dev/null +++ b/packages/vitec/Resources/Private/Language/locallang_ogimage.xlf @@ -0,0 +1,16 @@ + + + + + + OG Image + + + Generate Open Graph images (1200×630) for social media sharing. + + + OG Image Generator + + + + diff --git a/packages/vitec/Resources/Private/Layouts/Backend/Default.html b/packages/vitec/Resources/Private/Layouts/Backend/Default.html index a6d2195..f1447ac 100644 --- a/packages/vitec/Resources/Private/Layouts/Backend/Default.html +++ b/packages/vitec/Resources/Private/Layouts/Backend/Default.html @@ -1,7 +1,7 @@
- +

Hi

diff --git a/packages/vitec/Resources/Private/Templates/OgImage/Index.html b/packages/vitec/Resources/Private/Templates/OgImage/Index.html new file mode 100644 index 0000000..b21a80c --- /dev/null +++ b/packages/vitec/Resources/Private/Templates/OgImage/Index.html @@ -0,0 +1,288 @@ + + + + + + +
+
+

OG Image Generator

+
+
+ +
+ + +
+ + + +
+ + +
+
Quick Presets
+
+ + + +
+
+ + +
+
Background
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + + +
+
+ + +
+
Text
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
Label / Badge
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
Output
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ Preview (1200 × 630) + +
+
+
+ +
+ Click "Preview" to render +
+
+
+ +
+ + +
+
Recent saved images
+
+ + + + + + + + + + + +
FileCreated
{img.name}{img.created} + + View + +
+
+
+
+ +
+ +
+ +
+ + diff --git a/packages/vitec/Resources/Public/Icons/vitec-container.svg b/packages/vitec/Resources/Public/Icons/vitec-container.svg new file mode 100755 index 0000000..33141ec --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-container.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/vitec/Resources/Public/Icons/vitec-ogimage.svg b/packages/vitec/Resources/Public/Icons/vitec-ogimage.svg new file mode 100644 index 0000000..8b37ea4 --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-ogimage.svg @@ -0,0 +1,7 @@ + + + + OG + + + diff --git a/packages/vitec/Resources/Public/Javascript/og-image-preview.js b/packages/vitec/Resources/Public/Javascript/og-image-preview.js new file mode 100644 index 0000000..3cdfd7e --- /dev/null +++ b/packages/vitec/Resources/Public/Javascript/og-image-preview.js @@ -0,0 +1,173 @@ +/** + * OG Image Backend Module – live preview via AJAX + * EXT:vitec/Resources/Public/Javascript/og-image-preview.js + */ +(function () { + 'use strict'; + + /** Debounce helper */ + function debounce(fn, ms) { + let t; + return function (...args) { + clearTimeout(t); + t = setTimeout(() => fn.apply(this, args), ms); + }; + } + + /** Collect all form values into a FormData-compatible plain object */ + function collectFormData() { + const form = document.getElementById('vitecOgForm'); + if (!form) return {}; + const fd = new FormData(form); + const out = {}; + for (const [key, val] of fd.entries()) { + // key looks like "ogimage[something]" + const match = key.match(/^ogimage\[(.+)]$/); + if (match) { + out[key] = val; + } + } + return out; + } + + /** POST to the preview endpoint, update the */ + async function requestPreview() { + const form = document.getElementById('vitecOgForm'); + const spinner = document.getElementById('previewSpinner'); + const img = document.getElementById('ogPreviewImg'); + const placeholder = document.getElementById('ogPreviewPlaceholder'); + const dl = document.getElementById('previewDownload'); + + if (!form) return; + + if (spinner) spinner.classList.remove('d-none'); + + try { + const body = new URLSearchParams(collectFormData()); + const previewUrl = form.dataset.previewUrl || window.location.href; + + const resp = await fetch(previewUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + if (!resp.ok) throw new Error('Server error ' + resp.status); + + const json = await resp.json(); + + if (json.success && json.dataUri) { + img.src = json.dataUri; + img.style.display = 'block'; + if (placeholder) placeholder.style.display = 'none'; + + if (dl) { + dl.href = json.dataUri; + dl.classList.remove('d-none'); + } + } else { + console.warn('[OG Preview]', json.message); + } + } catch (e) { + console.error('[OG Preview] fetch failed:', e); + } finally { + if (spinner) spinner.classList.add('d-none'); + } + } + + const debouncedPreview = debounce(requestPreview, 600); + + // ── Wire up events once DOM is ready ────────────────────────────── + + document.addEventListener('DOMContentLoaded', function () { + + // Manual preview button + const btn = document.getElementById('btnPreview'); + if (btn) { + btn.addEventListener('click', requestPreview); + } + + // Auto-preview on any .js-live input change + document.querySelectorAll('.js-live').forEach(function (el) { + el.addEventListener('input', debouncedPreview); + el.addEventListener('change', debouncedPreview); + }); + + // Background type toggle (show colour vs image picker) + function syncBgType() { + const colorWrap = document.getElementById('bg-color-wrap'); + const imageWrap = document.getElementById('bg-image-wrap'); + const selected = document.querySelector('input.js-bg-type:checked'); + if (!selected) return; + if (selected.value === 'image') { + colorWrap && (colorWrap.style.display = 'none'); + imageWrap && (imageWrap.style.display = 'block'); + } else { + colorWrap && (colorWrap.style.display = 'block'); + imageWrap && (imageWrap.style.display = 'none'); + } + } + + document.querySelectorAll('input.js-bg-type').forEach(function (el) { + el.addEventListener('change', function () { + syncBgType(); + debouncedPreview(); + }); + }); + + const bgImageSelect = document.querySelector('select[name="ogimage[bg_image]"]'); + if (bgImageSelect) { + bgImageSelect.addEventListener('change', function () { + const imageTypeRadio = document.querySelector('input.js-bg-type[value="image"]'); + if (imageTypeRadio) { + imageTypeRadio.checked = true; + } + syncBgType(); + requestPreview(); + }); + } + + syncBgType(); // initialise on load + + // Hex text ↔ colour picker sync + document.querySelectorAll('.js-hex-sync').forEach(function (hexInput) { + const targetId = hexInput.dataset.target; + const picker = document.getElementById(targetId); + if (!picker) return; + + picker.addEventListener('input', function () { + hexInput.value = picker.value; + }); + + hexInput.addEventListener('input', function () { + const val = hexInput.value.trim(); + if (/^#[0-9a-fA-F]{6}$/.test(val)) { + picker.value = val; + picker.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + }); + + // Preset buttons + document.querySelectorAll('.js-preset').forEach(function (btn) { + btn.addEventListener('click', function () { + const bg = btn.dataset.bg; + const tc = btn.dataset.titleColor; + const sc = btn.dataset.subtitleColor; + + const bgInput = document.getElementById('bgColor'); + if (bgInput && bg) { bgInput.value = bg; bgInput.dispatchEvent(new Event('input', {bubbles:true})); } + + const tInput = document.getElementById('titleColor'); + if (tInput && tc) { tInput.value = tc; tInput.dispatchEvent(new Event('input', {bubbles:true})); } + + const sInput = document.getElementById('subtitleColor'); + if (sInput && sc) { sInput.value = sc; sInput.dispatchEvent(new Event('input', {bubbles:true})); } + + debouncedPreview(); + }); + }); + + }); + +})(); diff --git a/packages/vitec/ext_localconf.php b/packages/vitec/ext_localconf.php index 9be5ac9..f7fa26b 100755 --- a/packages/vitec/ext_localconf.php +++ b/packages/vitec/ext_localconf.php @@ -4,6 +4,14 @@ 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 +$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['vitec']['view'] = [ + 'templateRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Templates/'], + 'layoutRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Layouts/'], + 'partialRootPaths' => ['10' => 'EXT:vitec/Resources/Private/Partials/'], +]; + // Register VITEC Backend Layout data provider — 21 BE layouts, one per pages.layout (0..20) $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['vitec'] = \Evomedien\Vitec\View\BackendLayoutDataProvider::class;