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 );
@@ -162,8 +194,11 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar
162194 ];
163195 if (isset ($ allPackageConstraints [$ packageKey ]['dependencies ' ])) {
164196 foreach ($ allPackageConstraints [$ packageKey ]['dependencies ' ] as $ dependentPackageKey ) {
165- if (!in_array ($ dependentPackageKey , $ packageKeys , true )) {
166- if ($ this ->isComposerDependency ($ dependentPackageKey )) {
197+ $ extensionKey = $ this ->getPackageExtensionKey ($ dependentPackageKey );
198+ if (!in_array ($ dependentPackageKey , $ packageKeys , true )
199+ && !in_array ($ extensionKey , $ packageKeys , true )
200+ ) {
201+ if (!$ this ->isTypo3SystemOrCustomExtension ($ dependentPackageKey )) {
167202 // The given package has a dependency to a Composer package that has no relation to TYPO3
168203 // We can ignore those, when calculating the extension order
169204 continue ;
@@ -174,21 +209,30 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar
174209 1519931815
175210 );
176211 }
177- $ dependencies [$ packageKey ]['after ' ][] = $ dependentPackageKey ;
212+ $ dependencies [$ packageKey ]['after ' ][] = $ extensionKey ;
178213 }
179214 }
180215 if (isset ($ allPackageConstraints [$ packageKey ]['suggestions ' ])) {
181216 foreach ($ allPackageConstraints [$ packageKey ]['suggestions ' ] as $ suggestedPackageKey ) {
217+ $ extensionKey = $ this ->getPackageExtensionKey ($ suggestedPackageKey );
182218 // skip suggestions on not existing packages
183- if (in_array ($ suggestedPackageKey , $ packageKeys , true )) {
184- // Suggestions actually have never been meant to influence loading order.
185- // We misuse this currently, as there is no other way to influence the loading order
186- // for not-required packages (soft-dependency).
187- // When considering suggestions for the loading order, we might create a cyclic dependency
188- // if the suggested package already has a real dependency on this package, so the suggestion
189- // has do be dropped in this case and must *not* be taken into account for loading order evaluation .
190- $ dependencies [ $ packageKey ][ ' after-resilient ' ][] = $ suggestedPackageKey ;
219+ if (! in_array ($ suggestedPackageKey , $ packageKeys , true )
220+ && ! in_array ( $ extensionKey , $ packageKeys , true )
221+ ) {
222+ continue ;
223+ }
224+ if (! $ this -> isTypo3SystemOrCustomExtension ( $ extensionKey ?: $ suggestedPackageKey )) {
225+ // Ignore non TYPO3 extension packages for suggestion determination/ordering .
226+ continue ;
191227 }
228+
229+ // Suggestions actually have never been meant to influence loading order.
230+ // We misuse this currently, as there is no other way to influence the loading order
231+ // for not-required packages (soft-dependency).
232+ // When considering suggestions for the loading order, we might create a cyclic dependency
233+ // if the suggested package already has a real dependency on this package, so the suggestion
234+ // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
235+ $ dependencies [$ packageKey ]['after-resilient ' ][] = $ extensionKey ;
192236 }
193237 }
194238 }
@@ -255,25 +299,28 @@ protected function getDependencyArrayForPackage(PackageInterface $package, array
255299 foreach ($ dependentPackageConstraints as $ constraint ) {
256300 if ($ constraint instanceof PackageConstraint) {
257301 $ dependentPackageKey = $ constraint ->getValue ();
258- if (!in_array ($ dependentPackageKey , $ dependentPackageKeys , true ) && !in_array ($ dependentPackageKey , $ trace , true )) {
259- $ dependentPackageKeys [] = $ dependentPackageKey ;
302+ $ extensionKey = $ this ->getPackageExtensionKey ($ dependentPackageKey ) ?: $ dependentPackageKey ;
303+ if (!in_array ($ extensionKey , $ dependentPackageKeys , true )) {
304+ $ dependentPackageKeys [] = $ extensionKey ;
260305 }
261- if (!isset ($ this ->packages [$ dependentPackageKey ])) {
262- if ($ this ->isComposerDependency ($ dependentPackageKey )) {
306+
307+ if (!isset ($ this ->packages [$ extensionKey ])) {
308+ if (!$ this ->isTypo3SystemOrCustomExtension ($ extensionKey )) {
263309 // The given package has a dependency to a Composer package that has no relation to TYPO3
264310 // We can ignore those, when calculating the extension order
265311 continue ;
266312 }
313+
267314 throw new Exception (
268315 sprintf (
269316 'Package "%s" depends on package "%s" which does not exist. ' ,
270317 $ package ->getPackageKey (),
271- $ dependentPackageKey
318+ $ extensionKey
272319 ),
273320 1695119749
274321 );
275322 }
276- $ this ->getDependencyArrayForPackage ($ this ->packages [$ dependentPackageKey ], $ dependentPackageKeys , $ trace );
323+ $ this ->getDependencyArrayForPackage ($ this ->packages [$ extensionKey ], $ dependentPackageKeys , $ trace );
277324 }
278325 }
279326 return array_reverse ($ dependentPackageKeys );
@@ -292,9 +339,17 @@ protected function getSuggestionArrayForPackage(PackageInterface $package): arra
292339 foreach ($ suggestedPackageConstraints as $ constraint ) {
293340 if ($ constraint instanceof PackageConstraint) {
294341 $ suggestedPackageKey = $ constraint ->getValue ();
295- if (isset ($ this ->packages [$ suggestedPackageKey ])) {
296- $ suggestedPackageKeys [] = $ suggestedPackageKey ;
342+ $ extensionKey = $ this ->getPackageExtensionKey ($ suggestedPackageKey ) ?: $ suggestedPackageKey ;
343+ if (!$ this ->isTypo3SystemOrCustomExtension ($ suggestedPackageKey )) {
344+ // Suggested packages which are not installed or not a TYPO3 extension can be skipped for
345+ // sorting when not available.
346+ continue ;
297347 }
348+ if (!isset ($ this ->packages [$ extensionKey ])) {
349+ // Suggested extension is not available in test system installation (not symlinked), ignore it.
350+ continue ;
351+ }
352+ $ suggestedPackageKeys [] = $ extensionKey ;
298353 }
299354 }
300355 return array_reverse ($ suggestedPackageKeys );
@@ -308,15 +363,38 @@ protected function findFrameworkKeys(): array
308363 $ frameworkKeys = [];
309364 foreach ($ this ->packages as $ package ) {
310365 if ($ package ->getPackageMetaData ()->isFrameworkType ()) {
311- $ frameworkKeys [] = $ package ->getPackageKey ();
366+ $ frameworkKeys [] = $ this -> getPackageExtensionKey ( $ package -> getPackageKey ()) ?: $ package ->getPackageKey ();
312367 }
313368 }
314369 return $ frameworkKeys ;
315370 }
316371
317- protected function isComposerDependency (string $ packageKey ): bool
372+ /**
373+ * Determines if given composer package key is either a `typo3-cms-framework` or `typo3-cms-extension` package.
374+ */
375+ protected function isTypo3SystemOrCustomExtension (string $ packageKey ): bool
376+ {
377+ $ packageInfo = $ this ->getPackageInfo ($ packageKey );
378+ if ($ packageInfo === null ) {
379+ return false ;
380+ }
381+ return $ packageInfo ->isSystemExtension () || $ packageInfo ->isExtension ();
382+ }
383+
384+ /**
385+ * Returns package extension key. Returns empty string if not available.
386+ */
387+ protected function getPackageExtensionKey (string $ packageKey ): string
388+ {
389+ $ packageInfo = $ this ->getPackageInfo ($ packageKey );
390+ if ($ packageInfo === null ) {
391+ return '' ;
392+ }
393+ return $ packageInfo ->getExtensionKey ();
394+ }
395+
396+ protected function getPackageInfo (string $ packageKey ): ?PackageInfo
318397 {
319- $ packageInfo = $ this ->composerPackageManager ->getPackageInfo ($ packageKey );
320- return !(($ packageInfo ?->isSystemExtension() ?? false ) || ($ packageInfo ?->isExtension()));
398+ return $ this ->composerPackageManager ->getPackageInfo ($ packageKey );
321399 }
322400}
0 commit comments