2626use TYPO3 \CMS \Core \Utility \GeneralUtility ;
2727use TYPO3 \CMS \Core \Utility \PathUtility ;
2828use TYPO3 \TestingFramework \Composer \ComposerPackageManager ;
29+ use TYPO3 \TestingFramework \Composer \PackageInfo ;
30+ use TYPO3 \TestingFramework \Core \Functional \FunctionalTestCase ;
2931
3032/**
31- * Collection for extension packages to resolve their dependencies in a test-base.
32- * Most of the code has been duplicated and adjusted from `\TYPO3\CMS\Core\Package\PackageManager`.
33+ * Composer package collection to resolve extension dependencies for classic-mode based test instances.
34+ *
35+ * This class resolves extension dependencies for composer packages to sort classic-mode PackageStates,
36+ * which only takes TYPO3 extensions into account with a fallback to read composer information when the
37+ * `ext_emconf.php` file is missing.
38+ *
39+ * Most of the code has been duplicated and adjusted from {@see PackageManager}.
40+ *
41+ * Background:
42+ * ===========
43+ *
44+ * TYPO3 has the two installation mode "composer" and "classic". For the "composer" mode the package dependency handling
45+ * is mainly done by composer and dependency detection and sorting is purely based on composer.json information. "Classic"
46+ * mode uses only "ext_emconf.php" information to do the same job, not mixing it with the composer.json information when
47+ * available.
48+ *
49+ * Since TYPO3 v12 extensions installed in "composer" mode are not required to provide a "ext_emconf.php" anymore, which
50+ * makes them only installable within a "composer" mode installation. Agencies used to drop that file from local path
51+ * extensions in "composer" mode projects, because it is a not needed requirement for them and avoids maintenance of it.
52+ *
53+ * typo3/testing-framework builds "classic" mode functional test instances while used within composer installations only,
54+ * and introduced an extension sorting with this class to ensure to have a deterministic extension sorting like a real
55+ * "classic" mode installation would provide in case extensions are not manually provided in the correct order within
56+ * {@see FunctionalTestCase::$testExtensionToLoad} property.
57+ *
58+ * {@see PackageCollection} is based on the TYPO3 core {@see PackageManager} to provide a sorting for functional test
59+ * instances, falling back to use composer.json information in case no "ext_emconf.php" are given limiting it only to
60+ * TYPO3 compatible extensions (typo3-cms-framework and typo3-cms-extension composer package types).
3361 *
3462 * @phpstan-type PackageKey non-empty-string
3563 * @phpstan-type PackageName non-empty-string
@@ -54,6 +82,10 @@ public static function fromPackageStates(ComposerPackageManager $composerPackage
5482 {
5583 $ packages = [];
5684 foreach ($ packageStates as $ packageKey => $ packageStateConfiguration ) {
85+ // @todo Verify retrieving package information and throwing early exception after extension without
86+ // composer.json support has been dropped, even for simplified test fixture extensions. Think
87+ // about triggering deprecation for this case first, which may also breaking from a testing
88+ // perspective.
5789 $ packagePath = PathUtility::sanitizeTrailingSeparator (
5890 rtrim ($ basePath , '/ ' ) . '/ ' . $ packageStateConfiguration ['packagePath ' ]
5991 );
@@ -157,8 +189,11 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar
157189 ];
158190 if (isset ($ allPackageConstraints [$ packageKey ]['dependencies ' ])) {
159191 foreach ($ allPackageConstraints [$ packageKey ]['dependencies ' ] as $ dependentPackageKey ) {
160- if (!in_array ($ dependentPackageKey , $ packageKeys , true )) {
161- if ($ this ->isComposerDependency ($ dependentPackageKey )) {
192+ $ extensionKey = $ this ->getPackageExtensionKey ($ dependentPackageKey );
193+ if (!in_array ($ dependentPackageKey , $ packageKeys , true )
194+ && !in_array ($ extensionKey , $ packageKeys , true )
195+ ) {
196+ if (!$ this ->isTypo3SystemOrCustomExtension ($ dependentPackageKey )) {
162197 // The given package has a dependency to a Composer package that has no relation to TYPO3
163198 // We can ignore those, when calculating the extension order
164199 continue ;
@@ -169,21 +204,30 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar
169204 1519931815
170205 );
171206 }
172- $ dependencies [$ packageKey ]['after ' ][] = $ dependentPackageKey ;
207+ $ dependencies [$ packageKey ]['after ' ][] = $ extensionKey ;
173208 }
174209 }
175210 if (isset ($ allPackageConstraints [$ packageKey ]['suggestions ' ])) {
176211 foreach ($ allPackageConstraints [$ packageKey ]['suggestions ' ] as $ suggestedPackageKey ) {
212+ $ extensionKey = $ this ->getPackageExtensionKey ($ suggestedPackageKey );
177213 // skip suggestions on not existing packages
178- if (in_array ($ suggestedPackageKey , $ packageKeys , true )) {
179- // Suggestions actually have never been meant to influence loading order.
180- // We misuse this currently, as there is no other way to influence the loading order
181- // for not-required packages (soft-dependency).
182- // When considering suggestions for the loading order, we might create a cyclic dependency
183- // if the suggested package already has a real dependency on this package, so the suggestion
184- // has do be dropped in this case and must *not* be taken into account for loading order evaluation .
185- $ dependencies [ $ packageKey ][ ' after-resilient ' ][] = $ suggestedPackageKey ;
214+ if (! in_array ($ suggestedPackageKey , $ packageKeys , true )
215+ && ! in_array ( $ extensionKey , $ packageKeys , true )
216+ ) {
217+ continue ;
218+ }
219+ if (! $ this -> isTypo3SystemOrCustomExtension ( $ extensionKey ?: $ suggestedPackageKey )) {
220+ // Ignore non TYPO3 extension packages for suggestion determination/ordering .
221+ continue ;
186222 }
223+
224+ // Suggestions actually have never been meant to influence loading order.
225+ // We misuse this currently, as there is no other way to influence the loading order
226+ // for not-required packages (soft-dependency).
227+ // When considering suggestions for the loading order, we might create a cyclic dependency
228+ // if the suggested package already has a real dependency on this package, so the suggestion
229+ // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
230+ $ dependencies [$ packageKey ]['after-resilient ' ][] = $ extensionKey ;
187231 }
188232 }
189233 }
@@ -250,25 +294,28 @@ protected function getDependencyArrayForPackage(PackageInterface $package, array
250294 foreach ($ dependentPackageConstraints as $ constraint ) {
251295 if ($ constraint instanceof PackageConstraint) {
252296 $ dependentPackageKey = $ constraint ->getValue ();
253- if (!in_array ($ dependentPackageKey , $ dependentPackageKeys , true ) && !in_array ($ dependentPackageKey , $ trace , true )) {
254- $ dependentPackageKeys [] = $ dependentPackageKey ;
297+ $ extensionKey = $ this ->getPackageExtensionKey ($ dependentPackageKey ) ?: $ dependentPackageKey ;
298+ if (!in_array ($ extensionKey , $ dependentPackageKeys , true )) {
299+ $ dependentPackageKeys [] = $ extensionKey ;
255300 }
256- if (!isset ($ this ->packages [$ dependentPackageKey ])) {
257- if ($ this ->isComposerDependency ($ dependentPackageKey )) {
301+
302+ if (!isset ($ this ->packages [$ extensionKey ])) {
303+ if (!$ this ->isTypo3SystemOrCustomExtension ($ extensionKey )) {
258304 // The given package has a dependency to a Composer package that has no relation to TYPO3
259305 // We can ignore those, when calculating the extension order
260306 continue ;
261307 }
308+
262309 throw new Exception (
263310 sprintf (
264311 'Package "%s" depends on package "%s" which does not exist. ' ,
265312 $ package ->getPackageKey (),
266- $ dependentPackageKey
313+ $ extensionKey
267314 ),
268315 1695119749
269316 );
270317 }
271- $ this ->getDependencyArrayForPackage ($ this ->packages [$ dependentPackageKey ], $ dependentPackageKeys , $ trace );
318+ $ this ->getDependencyArrayForPackage ($ this ->packages [$ extensionKey ], $ dependentPackageKeys , $ trace );
272319 }
273320 }
274321 return array_reverse ($ dependentPackageKeys );
@@ -287,9 +334,17 @@ protected function getSuggestionArrayForPackage(PackageInterface $package): arra
287334 foreach ($ suggestedPackageConstraints as $ constraint ) {
288335 if ($ constraint instanceof PackageConstraint) {
289336 $ suggestedPackageKey = $ constraint ->getValue ();
290- if (isset ($ this ->packages [$ suggestedPackageKey ])) {
291- $ suggestedPackageKeys [] = $ suggestedPackageKey ;
337+ $ extensionKey = $ this ->getPackageExtensionKey ($ suggestedPackageKey ) ?: $ suggestedPackageKey ;
338+ if (!$ this ->isTypo3SystemOrCustomExtension ($ suggestedPackageKey )) {
339+ // Suggested packages which are not installed or not a TYPO3 extension can be skipped for
340+ // sorting when not available.
341+ continue ;
292342 }
343+ if (!isset ($ this ->packages [$ extensionKey ])) {
344+ // Suggested extension is not available in test system installation (not symlinked), ignore it.
345+ continue ;
346+ }
347+ $ suggestedPackageKeys [] = $ extensionKey ;
293348 }
294349 }
295350 return array_reverse ($ suggestedPackageKeys );
@@ -303,15 +358,38 @@ protected function findFrameworkKeys(): array
303358 $ frameworkKeys = [];
304359 foreach ($ this ->packages as $ package ) {
305360 if ($ package ->getPackageMetaData ()->isFrameworkType ()) {
306- $ frameworkKeys [] = $ package ->getPackageKey ();
361+ $ frameworkKeys [] = $ this -> getPackageExtensionKey ( $ package -> getPackageKey ()) ?: $ package ->getPackageKey ();
307362 }
308363 }
309364 return $ frameworkKeys ;
310365 }
311366
312- protected function isComposerDependency (string $ packageKey ): bool
367+ /**
368+ * Determines if given composer package key is either a `typo3-cms-framework` or `typo3-cms-extension` package.
369+ */
370+ protected function isTypo3SystemOrCustomExtension (string $ packageKey ): bool
371+ {
372+ $ packageInfo = $ this ->getPackageInfo ($ packageKey );
373+ if ($ packageInfo === null ) {
374+ return false ;
375+ }
376+ return $ packageInfo ->isSystemExtension () || $ packageInfo ->isExtension ();
377+ }
378+
379+ /**
380+ * Returns package extension key. Returns empty string if not available.
381+ */
382+ protected function getPackageExtensionKey (string $ packageKey ): string
383+ {
384+ $ packageInfo = $ this ->getPackageInfo ($ packageKey );
385+ if ($ packageInfo === null ) {
386+ return '' ;
387+ }
388+ return $ packageInfo ->getExtensionKey ();
389+ }
390+
391+ protected function getPackageInfo (string $ packageKey ): ?PackageInfo
313392 {
314- $ packageInfo = $ this ->composerPackageManager ->getPackageInfo ($ packageKey );
315- return !(($ packageInfo ?->isSystemExtension() ?? false ) || ($ packageInfo ?->isExtension()));
393+ return $ this ->composerPackageManager ->getPackageInfo ($ packageKey );
316394 }
317395}
0 commit comments