[TASK] Observe ext suggestions for ext loading order 05/30005/6
authorMarkus Klein <klein.t3@mfc-linz.at>
Sat, 10 May 2014 23:48:32 +0000 (01:48 +0200)
committerXavier Perseguers <xavier@typo3.org>
Tue, 10 Jun 2014 09:57:30 +0000 (11:57 +0200)
Let the DependencyResolver take the suggest-part of the composer.json
or ext_emconf.php files into account. This solves issues where it is
necessary to ensure a certain loading order of extensions without
having a real dependency between those.

Resolves: #57825
Releases: 6.2
Change-Id: Ia771813b7945409e5ddaec3f19da27239be16e67
Reviewed-on: https://review.typo3.org/30005
Reviewed-by: Loek Hilgersom
Tested-by: Loek Hilgersom
Reviewed-by: Thomas Maroschik
Reviewed-by: Fabien Udriot
Tested-by: Fabien Udriot
Reviewed-by: Stefan Neufeind
Reviewed-by: Xavier Perseguers
Tested-by: Xavier Perseguers
typo3/sysext/core/Classes/Package/DependencyResolver.php
typo3/sysext/core/Tests/Unit/Package/DependencyResolverTest.php

index 2419e49..77e496e 100644 (file)
@@ -140,6 +140,9 @@ class DependencyResolver {
                // Initialize the dependencies with FALSE
                $dependencyGraph = array_fill_keys($packageKeys, array_fill_keys($packageKeys, FALSE));
                foreach ($packageKeys as $packageKey) {
+                       if (!isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
+                               continue;
+                       }
                        $dependentPackageKeys = $packageStatesConfiguration[$packageKey]['dependencies'];
                        foreach ($dependentPackageKeys as $dependentPackageKey) {
                                if (!in_array($dependentPackageKey, $packageKeys)) {
@@ -151,10 +154,50 @@ class DependencyResolver {
                                $dependencyGraph[$packageKey][$dependentPackageKey] = TRUE;
                        }
                }
+               foreach ($packageKeys as $packageKey) {
+                       if (!isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
+                               continue;
+                       }
+                       $suggestedPackageKeys = $packageStatesConfiguration[$packageKey]['suggestions'];
+                       foreach ($suggestedPackageKeys as $suggestedPackageKey) {
+                               if (!in_array($suggestedPackageKey, $packageKeys)) {
+                                       continue;
+                               }
+                               // Check if there's no dependency of the suggestion to the package
+                               // Dependencies take precedence over suggestions
+                               $dependencies = $this->findPathInGraph($dependencyGraph, $suggestedPackageKey, $packageKey);
+                               if (empty($dependencies)) {
+                                       $dependencyGraph[$packageKey][$suggestedPackageKey] = TRUE;
+                               }
+                       }
+               }
                return $dependencyGraph;
        }
 
        /**
+        * Find any path in the graph from given start node to destination node
+        *
+        * @param array $graph Directed graph
+        * @param string $from Start node
+        * @param string $to Destination node
+        * @return array Nodes of the found path; empty if no path is found
+        */
+       protected function findPathInGraph(array $graph, $from, $to) {
+               foreach (array_keys(array_filter($graph[$from])) as $node) {
+                       if ($node === $to) {
+                               return array($from, $to);
+                       } else {
+                               $subPath = $this->findPathInGraph($graph, $node, $to);
+                               if (!empty($subPath)) {
+                                       array_unshift($subPath, $from);
+                                       return $subPath;
+                               }
+                       }
+               }
+               return array();
+       }
+
+       /**
         * Adds all root packages of current dependency graph as dependency
         * to all extensions.
         * This ensures that the framework extensions (aka sysext) are
index 6b69e00..c9daf61 100644 (file)
@@ -764,7 +764,125 @@ class DependencyResolverTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                        ),
                                ),
                        ),
+                       'Suggestions without reverse dependency' => array(
+                               array(
+                                       'A' => array(
+                                               'state' => 'active',
+                                               'suggestions' => array('B'),
+                                       ),
+                                       'B' => array(
+                                               'state' => 'active',
+                                       ),
+                                       'C' => array(
+                                               'state' => 'active',
+                                               'dependencies' => array('A')
+                                       ),
+                               ),
+                               array(
+                                       'A' => array(
+                                               'A' => FALSE,
+                                               'B' => TRUE,
+                                               'C' => FALSE,
+                                       ),
+                                       'B' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                       ),
+                                       'C' => array(
+                                               'A' => TRUE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                       ),
+                               ),
+                       ),
+                       'Suggestions with reverse dependency' => array(
+                               array(
+                                       'A' => array(
+                                               'state' => 'active',
+                                               'suggestions' => array('B'),
+                                       ),
+                                       'B' => array(
+                                               'state' => 'active',
+                                               'dependencies' => array('A')
+                                       ),
+                                       'C' => array(
+                                               'state' => 'active',
+                                               'dependencies' => array('A')
+                                       ),
+                               ),
+                               array(
+                                       'A' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                       ),
+                                       'B' => array(
+                                               'A' => TRUE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                       ),
+                                       'C' => array(
+                                               'A' => TRUE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                       ),
+                               ),
+                       ),
                );
        }
 
+       /**
+        * @return array
+        */
+       public function findPathInGraphReturnsCorrectPathDataProvider() {
+               return array(
+                       'Simple path' => array(
+                               array(
+                                       'A' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => TRUE),
+                                       'B' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE),
+                                       'C' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE),
+                                       'Z' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE)
+                               ),
+                           'A', 'Z',
+                           array('A', 'Z')
+                       ),
+                       'No path' => array(
+                               array(
+                                       'A' => array('A' => FALSE, 'B' => TRUE, 'C' => FALSE, 'Z' => FALSE),
+                                       'B' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE),
+                                       'C' => array('A' => FALSE, 'B' => TRUE, 'C' => FALSE, 'Z' => FALSE),
+                                       'Z' => array('A' => FALSE, 'B' => TRUE, 'C' => FALSE, 'Z' => FALSE)
+                               ),
+                               'A', 'C',
+                               array()
+                       ),
+                       'Longer path' => array(
+                               array(
+                                       'A' => array('A' => FALSE, 'B' => TRUE, 'C' => TRUE, 'Z' => TRUE),
+                                       'B' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE),
+                                       'C' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => TRUE),
+                                       'Z' => array('A' => FALSE, 'B' => FALSE, 'C' => FALSE, 'Z' => FALSE)
+                               ),
+                               'A', 'Z',
+                               array('A', 'C', 'Z')
+                       ),
+               );
+       }
+
+       /**
+        * @param array $graph
+        * @param string $from
+        * @param string $to
+        * @param array $expected
+        * @test
+        * @dataProvider findPathInGraphReturnsCorrectPathDataProvider
+        */
+       public function findPathInGraphReturnsCorrectPath(array $graph, $from, $to, array $expected) {
+               $dependencyResolver = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\DependencyResolver', array('dummy'));
+               $path = $dependencyResolver->_call('findPathInGraph', $graph, $from, $to);
+
+               $this->assertSame($expected, $path);
+       }
+
 }