diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4895e54..da4565d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,12 +12,13 @@ jobs: name: all tests runs-on: ubuntu-22.04 strategy: + fail-fast: false matrix: php: [ '8.2', '8.3', '8.4', '8.5'] TYPO3: [ '13', '14', '14-dev' ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install testing system run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s composerInstall @@ -52,14 +53,14 @@ jobs: - name: Acceptance Tests run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s acceptance -- --fail-fast - name: Archive acceptance tests results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: acceptance-test-reports-${{ matrix.php }}-${{ matrix.TYPO3 }} path: .Build/Web/typo3temp/var/tests/_output - name: Archive composer.lock - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: composer.lock-${{ matrix.php }}-${{ matrix.TYPO3 }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e0afab8f..dd1ef96c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check tag run: | @@ -39,6 +39,12 @@ jobs: with: php-version: 8.3 extensions: intl, mbstring, json, zip, curl + tools: composer:v2 + + - name: Set composer.json version and extra.typo3/cms.version + run: | + composer config "extra"."typo3/cms"."version" "${{ steps.get-version.outputs.version }}" \ + && composer validate --strict --no-check-lock --no-check-version - name: Install tailor run: composer global require typo3/tailor --prefer-dist --no-progress --no-suggest diff --git a/Build/Scripts/download-patch-from-gerrit.phpsh b/Build/Scripts/download-patch-from-gerrit.phpsh new file mode 100755 index 00000000..acd34e25 --- /dev/null +++ b/Build/Scripts/download-patch-from-gerrit.phpsh @@ -0,0 +1,142 @@ +#!/usr/bin/php +`, `@label ` and `@link ` +// to the patch file as meta data. + +$gerritId = null; +$showUsage = count($argv) < 2; +$patchesDirectory = __DIR__ . '/../../patches/'; + +if (!$showUsage) { + $gerritId = (int)$argv[1]; +} + +if ($showUsage || empty($gerritId)) { + $usage = [ + 'NAME', + "\tbin/" . basename($argv[0]) . ' -- downloads a patch from Gerrit', + '', + 'SYNOPSIS', + "\tbin/" . basename($argv[0] . ' '), + '', + 'DESCRIPTION', + "\tThis will download the latest patchset from", + "\thttps://review.typo3.org/c/Packages/TYPO3.CMS/+/id then", + "\tsplit it for the various typo3/cms-* packages and update", + "\tyour composer.json", + ]; + echo implode("\n", $usage) . "\n\n"; + exit(1); +} + +$change = file_get_contents('https://review.typo3.org/changes/' . $gerritId); +// Fix garbage at the beginning +if (strpos($change, ")]}'") === 0) { + $change = json_decode(trim(substr($change, 4)), true); +} +if (empty($change['subject'])) { + echo "Change $gerritId was not found.\n"; + exit(2); +} + +$subject = $change['subject']; +$normalizedSubject = preg_replace('/-+/', '-', str_replace( + [ + '`', + ' ', + '[!!!]', + '[wip]', + '[', + ']', + ':', + '$', + '/', + '\\', + ], + [ + '', + '-', + 'breaking-', + '', + '', + '-', + '_', + '', + '', + '', + ], + mb_strtolower($subject) +)); +echo "Subject is '$subject'\n"; + +$patch = base64_decode(file_get_contents('https://review.typo3.org/changes/' . $gerritId . '/revisions/current/patch')); +$patchMessage = null; +$patches = []; +do { + $nextPatchPos = strpos($patch, 'diff --git', 1); + if ($nextPatchPos === false) { + $buffer = $patch; + $patch = ''; + } else { + $buffer = substr($patch, 0, $nextPatchPos); + $patch = substr($patch, $nextPatchPos); + } + if ($patchMessage === null) { + $patchMessage = $buffer; + continue; + } + // Ignore any file not part of a system extension, for example monorepository build system files. + if (!preg_match('#^diff --git a/typo3/sysext/([^/]+)/([^ ]+)#', $buffer, $matches)) { + continue; + } + $sysExtRelativeFilename = $matches[2]; + if (str_starts_with($sysExtRelativeFilename, 'Tests/')) { + // Skip any test related files, which are not included in distribution + // archives of composer packages and are not patchable. + continue; + } + $sysext = $matches[1]; + $package = match(true) { + str_starts_with($sysext, 'theme-') => 'typo3/' . str_replace('_', '-', $sysext), + default => 'typo3/cms-' . str_replace('_', '-', $sysext), + }; + $patchPrologue = [ + '@package ' . $package, + '@label ' . $subject, + '@link ' . 'https://review.typo3.org/c/Packages/TYPO3.CMS/+/' . $gerritId, + '', + ]; + if (!isset($patches[$package])) { + $patches[$package] = [ + implode(PHP_EOL, $patchPrologue) . PHP_EOL . $patchMessage, + ]; + } + + // Fix the patch + $prefix = 'typo3/sysext/' . $matches[1]; + $file = $matches[2]; + $buffer = str_replace(' a/' . $prefix . '/' . $file, ' a/' . $file, $buffer); + $buffer = str_replace(' b/' . $prefix . '/' . $file, ' b/' . $file, $buffer); + $patches[$package][] = $buffer; +} while (!empty($patch)); + +$composerChanges = []; +foreach ($patches as $package => $chunks) { + if (!is_dir($patchesDirectory)) { + @mkdir($patchesDirectory, 0775, true); + } + $content = implode('', $chunks); + $patchFileName = str_replace('/', '-', $package) . '_' . $gerritId . '_' . $normalizedSubject . '.patch'; + file_put_contents($patchesDirectory . $patchFileName, $content); + echo "Created patch '" . $patchesDirectory . $patchFileName . "'\n"; + + $composerChanges[$package] = [ + $subject => 'patches/' . $patchFileName, + ]; +} + +echo "\n\nRun \"./composer install\" to install and apply missing patches.\n\n"; diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 6191c016..6fb77c08 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -131,6 +131,7 @@ Options: - composer: "composer" command dispatcher, to execute various composer commands - composerInstall: "composer install", handy if host has no PHP, uses composer cache of users home - composerValidate: "composer validate" + - downloadGerritPatch: Download TYPO3 Gerrit change and transform it to composer patch files in "patches/" - functional: functional tests - lint: PHP linting - phpstan: phpstan tests @@ -283,7 +284,7 @@ CORE_ROOT="${PWD}" # Option defaults TEST_SUITE="help" -COMPOSER_ROOT_VERSION="2.3.7-dev" +COMPOSER_ROOT_VERSION="3.2.4-dev" DBMS="mariadb" DBMS_VERSION="" PHP_VERSION="8.2" @@ -597,9 +598,9 @@ case ${TEST_SUITE} in php -v | grep '^PHP'; if [ "${TYPO3}" == "14-dev" ]; then - composer require typo3/cms-core:14.2.x-dev --dev -W --no-progress --no-interaction + composer require typo3/cms-core:14.3.x-dev --dev -W --no-progress --no-interaction elif [ ${TYPO3} -eq 14 ]; then - composer require typo3/cms-core:^14.1 --dev -W --no-progress --no-interaction + composer require typo3/cms-core:^14.2 --dev -W --no-progress --no-interaction else composer require typo3/cms-core:^13.4 ichhabrecht/content-defender --dev -W --no-progress --no-interaction fi @@ -616,11 +617,11 @@ case ${TEST_SUITE} in ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-validate-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c " php -v | grep '^PHP'; if [ "${TYPO3}" == "14-dev" ]; then - composer require typo3/cms-core:14.2.x-dev --dev -W --no-progress --no-interaction + composer require 'typo3/cms-core':'14.3.*@dev' --dev -W --no-progress --no-interaction elif [ ${TYPO3} -eq 14 ]; then - composer require typo3/cms-core:^14.1 --dev -W --no-progress --no-interaction + composer require 'typo3/cms-core':'^14.1' --dev -W --no-progress --no-interaction else - composer require typo3/cms-core:^13.4 ichhabrecht/content-defender --dev -W --no-progress --no-interaction + composer require 'typo3/cms-core':'^13.4' 'ichhabrecht/content-defender' --dev -W --no-progress --no-interaction fi composer validate " @@ -628,6 +629,11 @@ case ${TEST_SUITE} in cp composer.json composer.json.testing mv composer.json.orig composer.json ;; + downloadGerritPatch) + COMMAND=(php -dxdebug.mode=off Build/Scripts/download-patch-from-gerrit.phpsh "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; functional) COMMAND=(.Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml --exclude-group not-${DBMS} "$@") case ${DBMS} in diff --git a/Build/sites/autogenerated-2-c81e728d9d4c2f636f067f89cc14862c/config.yaml b/Build/sites/autogenerated-2-c81e728d9d4c2f636f067f89cc14862c/config.yaml new file mode 100644 index 00000000..74296369 --- /dev/null +++ b/Build/sites/autogenerated-2-c81e728d9d4c2f636f067f89cc14862c/config.yaml @@ -0,0 +1,14 @@ +base: autogenerated-2 +dependencies: { } +errorHandling: { } +languages: + - + title: English + enabled: true + languageId: 0 + base: / + locale: en_US.UTF-8 + navigationTitle: English + flag: us +rootPageId: 2 +routes: { } diff --git a/composer.json b/composer.json index 55ef6636..cd3db31d 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "b13/container", - "description": "Create Custom Container Content Elements for TYPO3", + "description": "Container Content Elements - Create Custom Container Content Elements for TYPO3", "type": "typo3-cms-extension", "homepage": "https://b13.com", "license": ["GPL-2.0-or-later"], @@ -24,34 +24,39 @@ "bin-dir": ".Build/bin", "allow-plugins": { "typo3/class-alias-loader": true, - "typo3/cms-composer-installers": true - } + "typo3/cms-composer-installers": true, + "vaimo/composer-patches": true + }, + "sort-packages": true }, "require-dev": { "b13/container-example": "dev-task/v4", - "typo3/cms-install": "^13.4 || ^14.1 || 14.2.x-dev", - "typo3/cms-fluid-styled-content": "^13.4 || ^14.1 || 14.2.x-dev", - "typo3/cms-info": "^13.4 || ^14.1 || 14.2.x-dev", - "typo3/cms-workspaces": "^13.4 || ^14.1 || 14.2.x-dev", - "typo3/testing-framework": "^9.1", - "phpstan/phpstan": "^1.10", - "typo3/coding-standards": "^0.5.5", - "friendsofphp/php-cs-fixer": "^3.51", "codeception/codeception": "^5.1", "codeception/module-asserts": "^3.0", - "codeception/module-webdriver": "^4.0", "codeception/module-db": "^3.1", - "phpunit/phpunit": "^11.3" - }, - "replace": { - "typo3-ter/container": "self.version" + "codeception/module-webdriver": "^4.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3", + "typo3/cms-fluid-styled-content": "^13.4 || ^14.2 || 14.3.*@dev", + "typo3/cms-info": "^13.4 || ^14.2 || 14.3.*@dev", + "typo3/cms-install": "^13.4 || ^14.2 || 14.3.*@dev", + "typo3/cms-workspaces": "^13.4 || ^14.2 || 14.3.*@dev", + "typo3/coding-standards": "^0.5.5", + "typo3/testing-framework": "^9.5", + "vaimo/composer-patches": "^6.0.1" }, "extra": { "typo3/cms": { "cms-package-dir": "{$vendor-dir}/typo3/cms", "web-dir": ".Build/Web", "app-dir": ".Build", - "extension-key": "container" - } + "extension-key": "container", + "version": "3.2.4-dev", + "Package": { + "providesPackages": [] + } + }, + "patches-search": "patches/" } } diff --git a/ext_emconf.php b/ext_emconf.php index 5ca7385f..8890f07f 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'uploadfolder' => false, 'createDirs' => '', 'clearCacheOnLoad' => true, - 'version' => '3.2.2', + 'version' => '3.2.4', 'constraints' => [ 'depends' => ['typo3' => '13.4.26-14.99.99'], 'conflicts' => [], diff --git a/patches/typo3-cms-backend_93484_bugfix-strip-title-from-package-description-if-extracted.patch b/patches/typo3-cms-backend_93484_bugfix-strip-title-from-package-description-if-extracted.patch new file mode 100644 index 00000000..1d385ba3 --- /dev/null +++ b/patches/typo3-cms-backend_93484_bugfix-strip-title-from-package-description-if-extracted.patch @@ -0,0 +1,43 @@ +@package typo3/cms-backend +@label [BUGFIX] Strip title from package description if extracted +@link https://review.typo3.org/c/Packages/TYPO3.CMS/+/93484 +@version ~14.2.0 + +From 17f8256acba6eb1c5b4b3e2732b63dc891279ad0 Mon Sep 17 00:00:00 2001 +From: Benjamin Kott +Date: Tue, 31 Mar 2026 11:24:01 +0200 +Subject: [PATCH] [BUGFIX] Strip title from package description if extracted + +When a composer description contains " - ", the part before +the separator is used as the package title. However, the +description was not updated and still contained the full +string including the title prefix, leading to duplicate +information when both title and description are displayed. + +The description is now set to only the part after the +separator when a title is extracted. + +Resolves: #109435 +Releases: main +Change-Id: I2a7a57ca18b750fa26fc967fe3cc5f6df07f3728 +Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/93484 +Reviewed-by: Benjamin Franzke +Reviewed-by: Benni Mack +Tested-by: Benjamin Franzke +Tested-by: Benni Mack +Tested-by: core-ci +--- + +diff --git a/Classes/Controller/AboutController.php b/Classes/Controller/AboutController.php +index 7ffdf16..c8ca11c 100644 +--- a/Classes/Controller/AboutController.php ++++ b/Classes/Controller/AboutController.php +@@ -81,7 +81,7 @@ + } + $extensions[] = [ + 'key' => $package->getPackageKey(), +- 'title' => $package->getPackageMetaData()->getDescription(), ++ 'title' => $package->getPackageMetaData()->getTitle(), + 'authors' => $package->getValueFromComposerManifest('authors'), + ]; + } diff --git a/patches/typo3-cms-core_93484_bugfix-strip-title-from-package-description-if-extracted.patch b/patches/typo3-cms-core_93484_bugfix-strip-title-from-package-description-if-extracted.patch new file mode 100644 index 00000000..88d0cb88 --- /dev/null +++ b/patches/typo3-cms-core_93484_bugfix-strip-title-from-package-description-if-extracted.patch @@ -0,0 +1,53 @@ +@package typo3/cms-core +@label [BUGFIX] Strip title from package description if extracted +@link https://review.typo3.org/c/Packages/TYPO3.CMS/+/93484 +@version ~14.2.0 +@after typo3-cms-core_93530_bugfix-avoid-undefined-property_-stdclass__version-warning.patch + +From 17f8256acba6eb1c5b4b3e2732b63dc891279ad0 Mon Sep 17 00:00:00 2001 +From: Benjamin Kott +Date: Tue, 31 Mar 2026 11:24:01 +0200 +Subject: [PATCH] [BUGFIX] Strip title from package description if extracted + +When a composer description contains " - ", the part before +the separator is used as the package title. However, the +description was not updated and still contained the full +string including the title prefix, leading to duplicate +information when both title and description are displayed. + +The description is now set to only the part after the +separator when a title is extracted. + +Resolves: #109435 +Releases: main +Change-Id: I2a7a57ca18b750fa26fc967fe3cc5f6df07f3728 +Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/93484 +Reviewed-by: Benjamin Franzke +Reviewed-by: Benni Mack +Tested-by: Benjamin Franzke +Tested-by: Benni Mack +Tested-by: core-ci +--- + +diff --git a/Classes/Package/Package.php b/Classes/Package/Package.php +index b163b85..383c126 100644 +--- a/Classes/Package/Package.php ++++ b/Classes/Package/Package.php +@@ -143,13 +143,15 @@ + { + $this->packageMetaData = new MetaData($this->getPackageKey()); + $description = (string)$this->getValueFromComposerManifest('description'); +- $this->packageMetaData->setDescription($description); + $descriptionParts = explode(' - ', $description, 2); +- $title = $description; + if (count($descriptionParts) === 2) { + $title = $descriptionParts[0]; ++ $description = $descriptionParts[1]; ++ } else { ++ $title = $description; + } + $this->packageMetaData->setTitle($title); ++ $this->packageMetaData->setDescription($description); + $this->packageMetaData->setPackageType((string)$this->getValueFromComposerManifest('type')); + $isFrameworkPackage = $this->packageMetaData->isFrameworkType(); + $version = (string)($this->getValueFromComposerManifest('version') ?? '1.0.0'); diff --git a/patches/typo3-cms-core_93530_bugfix-avoid-undefined-property_-stdclass__version-warning.patch b/patches/typo3-cms-core_93530_bugfix-avoid-undefined-property_-stdclass__version-warning.patch new file mode 100644 index 00000000..2896bafa --- /dev/null +++ b/patches/typo3-cms-core_93530_bugfix-avoid-undefined-property_-stdclass__version-warning.patch @@ -0,0 +1,61 @@ +@package typo3/cms-core +@label [BUGFIX] Avoid `Undefined property: stdClass::$version` warning +@link https://review.typo3.org/c/Packages/TYPO3.CMS/+/93530 +@version ~14.2.0 + +From 0672a9436796d0b8cc047fe29e611a654dbea6e7 Mon Sep 17 00:00:00 2001 +From: Stefan Bürk +Date: Thu, 02 Apr 2026 16:34:35 +0200 +Subject: [PATCH] [BUGFIX] Avoid `Undefined property: stdClass::$version` warning + +With [1] resolving #108345 #96388 extension need to have +`version` set in the extension `composer.json` for classic +mode installation. + +In case the optional `version` key is not set, which is +recommended to avoid by composer, this leads to a native +php warning: + + Undefined property: stdClass::$version in + vendor/typo3/cms-core/Classes/Package/PackageManager.php:939 + +This has been detected trying to make a extension TYPO3 v14 ready +with v13/v14 dual support and keeping the `ext_emconf.php`, which +emits the warning during functional test execution and should at +least respect that it is optional and possible not available. + +Does not fix the fact that the deprecation is still triggered, +which is the topic of another change to discuss and resolve. + +Let's make at least the warning to vanish. + +[1] https://review.typo3.org/c/Packages/TYPO3.CMS/+/91908 + +Resolves: #109473 +Related: #108345 +Related: #96388 +Releases: main +Change-Id: Iac794747b3afe837a7b89e369233fd9f891aa714 +Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/93530 +Tested-by: Oliver Klee +Tested-by: Garvin Hicking +Reviewed-by: Garvin Hicking +Tested-by: Stefan Bürk +Reviewed-by: Stefan Bürk +Tested-by: core-ci +Reviewed-by: Oliver Klee +--- + +diff --git a/Classes/Package/PackageManager.php b/Classes/Package/PackageManager.php +index 3d9496a..dd9b427 100644 +--- a/Classes/Package/PackageManager.php ++++ b/Classes/Package/PackageManager.php +@@ -936,7 +936,7 @@ + private function isComposerOnlyCapable(\stdClass $manifest): bool + { + return isset($manifest->extra->{'typo3/cms'}->Package->providesPackages) +- && $manifest->version !== null; ++ && ($manifest->version ?? null) !== null; + } + + /** diff --git a/patches/typo3-cms-core_93536_task-allow-extension-version-declaration-in-extra.version.patch b/patches/typo3-cms-core_93536_task-allow-extension-version-declaration-in-extra.version.patch new file mode 100644 index 00000000..a9ac8fda --- /dev/null +++ b/patches/typo3-cms-core_93536_task-allow-extension-version-declaration-in-extra.version.patch @@ -0,0 +1,339 @@ +@package typo3/cms-core +@label [TASK] Allow extension version declaration in extra.version +@link https://review.typo3.org/c/Packages/TYPO3.CMS/+/93536 +@version ~14.2.0 +@after typo3-cms-core_93484_bugfix-strip-title-from-package-description-if-extracted.patch + +From ccce176b8173fbb59d79312a0723b599e8838682 Mon Sep 17 00:00:00 2001 +From: Helmut Hummel +Date: Fri, 03 Apr 2026 12:08:32 +0200 +Subject: [PATCH] [TASK] Allow extension version declaration in extra.version + +Using the top level "version" field for a TYPO3 extension version +for classic mode has the disadvantage, that Composer pulls in +this version also for branches (aka. dev versions). +This would then either mean for extension authors that they need +to update this field constantly for branches and/ or releases, +which would have a bigger impact on behaviour in Composer managed +TYPO3 systems and extension authors than initially intended. + +Therefore it is now possible to declare the version in extra section. + +This field must still match the composer version that is tagged. + +Releases: main +Resolves: #109482 +Change-Id: I96cbdb175dc46ff5b3b1f863663412395fd4469c +Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/93536 +Reviewed-by: Garvin Hicking +Reviewed-by: Georg Ringer +Tested-by: Garvin Hicking +Reviewed-by: Stefan Bürk +Tested-by: Georg Ringer +Tested-by: core-ci +Tested-by: Stefan Bürk +--- + +diff --git a/Classes/Package/Package.php b/Classes/Package/Package.php +index 383c126..85739b7 100644 +--- a/Classes/Package/Package.php ++++ b/Classes/Package/Package.php +@@ -150,16 +150,17 @@ + } else { + $title = $description; + } ++ $manifest = $this->getValueFromComposerManifest(); + $this->packageMetaData->setTitle($title); + $this->packageMetaData->setDescription($description); +- $this->packageMetaData->setPackageType((string)$this->getValueFromComposerManifest('type')); ++ $this->packageMetaData->setPackageType($manifest->type ?? ''); + $isFrameworkPackage = $this->packageMetaData->isFrameworkType(); +- $version = (string)($this->getValueFromComposerManifest('version') ?? '1.0.0'); ++ $version = $manifest->extra->{'typo3/cms'}->{'version'} ?? $manifest->version ?? '1.0.0+no-version-set'; + if ($isFrameworkPackage) { + $version = str_replace('-dev', '', (new Typo3Version())->getVersion()); + } + $this->packageMetaData->setVersion($version); +- $requirements = $this->getValueFromComposerManifest('require'); ++ $requirements = $manifest->require ?? null; + if ($requirements !== null) { + foreach ($requirements as $packageName => $versionConstraints) { + if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) { +@@ -174,7 +175,7 @@ + ); + } + } +- $suggestions = $this->getValueFromComposerManifest('suggest'); ++ $suggestions = $manifest->suggest ?? null; + if ($suggestions !== null) { + foreach ($suggestions as $packageName => $description) { + if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) { +@@ -184,7 +185,7 @@ + $this->packageMetaData->addConstraint($constraint); + } + } +- $conflicts = $this->getValueFromComposerManifest('conflict'); ++ $conflicts = $manifest->conflict ?? null; + if ($conflicts !== null) { + foreach ($conflicts as $packageName => $versionConstraints) { + if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) { +diff --git a/Classes/Package/PackageManager.php b/Classes/Package/PackageManager.php +index dd9b427..cc419ff 100644 +--- a/Classes/Package/PackageManager.php ++++ b/Classes/Package/PackageManager.php +@@ -936,7 +936,10 @@ + private function isComposerOnlyCapable(\stdClass $manifest): bool + { + return isset($manifest->extra->{'typo3/cms'}->Package->providesPackages) +- && ($manifest->version ?? null) !== null; ++ && ( ++ ($manifest->version ?? null) !== null ++ || isset($manifest->extra->{'typo3/cms'}->version) ++ ); + } + + /** +diff --git a/Documentation/Changelog/14.2/Deprecation-108345-Deprecation-of-ext-emconf-php.rst b/Documentation/Changelog/14.2/Deprecation-108345-Deprecation-of-ext-emconf-php.rst +index cea425f..7ca1e9b 100644 +--- a/Documentation/Changelog/14.2/Deprecation-108345-Deprecation-of-ext-emconf-php.rst ++++ b/Documentation/Changelog/14.2/Deprecation-108345-Deprecation-of-ext-emconf-php.rst +@@ -26,7 +26,6 @@ + "name": "vendor/example", + "type": "typo3-cms-extension", + "description": "example", +- "version": "1.0.0", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "^14.2", +@@ -36,6 +35,7 @@ + "extra": { + "typo3/cms": { + "extension-key": "example_extension", ++ "version": "1.0.0", + "Package": { + "providesPackages": { + "symfony/dotenv": "" +@@ -52,7 +52,6 @@ + "name": "vendor/example2", + "type": "typo3-cms-extension", + "description": "example", +- "version": "1.0.0", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "^14.2" +@@ -60,6 +59,7 @@ + "extra": { + "typo3/cms": { + "extension-key": "example2_extension", ++ "version": "1.0.0", + "Package": { + "providesPackages": {} + } +@@ -68,10 +68,20 @@ + } + + For compatibility with TYPO3 classic mode, third-party extensions +-must set the exact extension version in the top-level `"version"` field +-of `composer.json`. This version should match the version previously ++must set the exact extension version in `extra.typo3/cms.version` ++or in the top level `version` field of `composer.json`. ++This version must match the version previously + defined in `ext_emconf.php` and the released Git tag. + ++Fixture extensions used in tests can set any version number, for example `1.0.0`, ++but a version number must still be provided to avoid deprecation messages. ++ ++During testing, the version number is not evaluated. ++ ++TYPO3 Core extensions may omit the version number ++in `composer.json`, because their version can and will be derived from ++php`\TYPO3\CMS\Core\Information\Typo3Version`. ++ + If an extension depends on regular Composer packages, these packages + must be declared in + `extra.typo3/cms.Package.providesPackages`. +@@ -81,6 +91,13 @@ + to avoid deprecation messages and to declare future compatibility + with TYPO3 classic mode. + ++If strict `composer.json` validation is required and the extension is published ++to Packagist as well, where setting the top level `version` field is not recommended, ++it is recommended to set the version via `extra.typo3/cms.version`. ++ ++If the `version` field is set anyway, it is recommended to omit `extra.typo3/cms.version` ++to avoid redundant data points. ++ + Impact + ====== + +@@ -88,7 +105,8 @@ + + TYPO3 classic installations will trigger a deprecation message + for extensions that still ship `ext_emconf.php` but do not yet define +-both the `"version"` field and `providesPackages` in `composer.json`. ++both `extra.typo3/cms.version` (or `"version"` field ) *and* `providesPackages` ++in `composer.json`. + + Affected installations + ====================== +@@ -109,6 +127,6 @@ + For the time being, `ext_emconf.php` may still need to be kept for + third-party tooling such as TYPO3 TER or Tailor. However, once the + required metadata is correctly defined in `composer.json`, +-TYPO3 will no longer need to evaluate `ext_emconf.php`. ++TYPO3 will no longer evaluate `ext_emconf.php`. + + .. index:: ext:core, NotScanned +diff --git a/Documentation/Changelog/14.2/Feature-108345-No-ext-em-conf-in-classic-mode.rst b/Documentation/Changelog/14.2/Feature-108345-No-ext-em-conf-in-classic-mode.rst +index 9a05b80..ef8c299 100644 +--- a/Documentation/Changelog/14.2/Feature-108345-No-ext-em-conf-in-classic-mode.rst ++++ b/Documentation/Changelog/14.2/Feature-108345-No-ext-em-conf-in-classic-mode.rst +@@ -18,7 +18,7 @@ + + This is now resolved by allowing an extension's `composer.json` + to contain information that previously had to be defined in +-`ext_emconf.php`. ++`ext_emconf.php`: + + 1. Extension title + 2. Extension version +@@ -30,16 +30,15 @@ + See :ref:`feature-108653-1767199420` for how the extension title can also be set + in `composer.json`. + +-The version number can be set in the regular `"version"` field +-in `composer.json`. +- + Extension version + ----------------- + ++The version number can be set in `extra.typo3/cms.version` or alternatively ++in the regular `"version"` field in `composer.json`. ++ + For third-party extensions to be compatible with TYPO3 classic mode, +-this field must now be set to the exact version previously defined in `ext_emconf.php` +-and must match the version in the Git tag +-(for example, when publishing to Packagist). ++this version must now be set to the exact version previously defined in `ext_emconf.php` ++and must match the version in the Git tag (for example, when publishing to Packagist). + + Fixture extensions used in tests can set any version number, for example `1.0.0`, + but a version number must still be provided to avoid deprecation messages. +@@ -57,7 +56,7 @@ + a version range. + + `composer.json` also contains a field for specifying dependencies +-by using a Composer package name with a version range. ++using a Composer package name with a version range. + However, there is no direct way to distinguish whether such a package name + refers to another TYPO3 extension or to a regular Composer package + that should be installed from Packagist. +@@ -113,7 +112,6 @@ + "name": "vendor/example", + "type": "typo3-cms-extension", + "description": "example", +- "version": "1.0.0", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "^14.2", +@@ -123,6 +121,7 @@ + "extra": { + "typo3/cms": { + "extension-key": "example_extension", ++ "version": "1.0.0", + "Package": { + "providesPackages": { + "symfony/dotenv": "" +@@ -151,7 +150,6 @@ + "name": "vendor/example", + "type": "typo3-cms-extension", + "description": "example", +- "version": "1.0.0", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "^14.2", +@@ -160,6 +158,7 @@ + "extra": { + "typo3/cms": { + "extension-key": "example_extension", ++ "version": "1.0.0", + "Package": { + "providesPackages": {} + } +@@ -167,13 +166,41 @@ + } + } + ++If the `version` field is set anyway, the version is populated from there ++and `extra.typo3/cms.version` can be omitted: ++ ++.. code-block:: json ++ ++ { ++ "name": "vendor/example", ++ "version": "1.0.0", ++ "type": "typo3-cms-extension", ++ "description": "example", ++ "license": "GPL-2.0-or-later", ++ "require": { ++ "typo3/cms-core": "^14.2", ++ "vendor/other-example": "*", ++ "symfony/dotenv": "^8.0" ++ }, ++ "extra": { ++ "typo3/cms": { ++ "extension-key": "example_extension", ++ "Package": { ++ "providesPackages": { ++ "symfony/dotenv": "" ++ } ++ } ++ } ++ } ++ } ++ + Be aware that keeping `ext_emconf.php`, while no longer directly required + by TYPO3, may still be necessary for some tools, + such as Tailor or TYPO3 TER. Therefore, for the time being, it is recommended + to keep the file and ensure that its information stays in sync + with `composer.json` as outlined above. + +-However, TYPO3 will **not** evaluate `ext_emconf.php` any more if the `version` field ++However, TYPO3 will **not** evaluate `ext_emconf.php` anymore if the `version` field + and `providesPackages` are correctly defined in `composer.json`. + + Impact +@@ -181,7 +208,7 @@ + + Extensions can now omit `ext_emconf.php` in TYPO3 classic mode. + A deprecation message is shown during cache warm-up when `ext_emconf.php` +-is present and `composer.json` is not yet future-proof, ++is present and `composer.json` is not yet future-proof + because it does not contain the `version` and `providesPackages` definitions. + + .. index:: ext:core +diff --git a/Tests/Unit/Utility/Fixtures/ext_emconf.php b/Tests/Unit/Utility/Fixtures/ext_emconf.php +deleted file mode 100644 +index 8288584..0000000 +--- a/Tests/Unit/Utility/Fixtures/ext_emconf.php ++++ /dev/null +@@ -1,19 +0,0 @@ +- '', +- 'description' => 'This is a fixture extension configuration file used for unit tests.', +- 'category' => '', +- 'state' => 'stable', +- 'author' => '', +- 'author_email' => '', +- 'author_company' => '', +- 'version' => '14.3.0', +- 'constraints' => [ +- 'depends' => [], +- 'conflicts' => [], +- 'suggests' => [], +- ], +-];