635110c471441bc2744e294d01215d64ecbd856e
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / DependencyResolver.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Core\Bootstrap;
18 use TYPO3\CMS\Core\Service\DependencyOrderingService;
19
20 /**
21 * This class takes care about dependencies between packages.
22 * It provides functionality to resolve dependencies and to determine
23 * the crucial loading order of the packages.
24 *
25 * @author Markus Klein <markus.klein@typo3.org>
26 */
27 class DependencyResolver {
28
29 /**
30 * Folder with framework extensions
31 */
32 const SYSEXT_FOLDER = 'typo3/sysext';
33
34 /**
35 * @var DependencyOrderingService
36 */
37 protected $dependencyOrderingService;
38
39 /**
40 * @param DependencyOrderingService $dependencyOrderingService
41 */
42 public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService) {
43 $this->dependencyOrderingService = $dependencyOrderingService;
44 }
45
46 /**
47 * @param array $packageStatesConfiguration
48 * @return array Returns the packageStatesConfiguration sorted by dependencies
49 * @throws \UnexpectedValueException
50 */
51 public function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration) {
52 // We just want to consider active packages
53 $activePackageStatesConfiguration = array_filter($packageStatesConfiguration, function($packageState) {
54 return isset($packageState['state']) && $packageState['state'] === 'active';
55 });
56 $inactivePackageStatesConfiguration = array_diff_key($packageStatesConfiguration, $activePackageStatesConfiguration);
57
58 $sortedPackageKeys = $this->dependencyOrderingService->calculateOrder($this->buildDependencyGraph($activePackageStatesConfiguration));
59
60 // Reorder the package states according to the loading order
61 $newPackageStatesConfiguration = array();
62 foreach ($sortedPackageKeys as $packageKey) {
63 $newPackageStatesConfiguration[$packageKey] = $packageStatesConfiguration[$packageKey];
64 }
65
66 // Append the inactive configurations again
67 $newPackageStatesConfiguration = array_merge($newPackageStatesConfiguration, $inactivePackageStatesConfiguration);
68
69 return $newPackageStatesConfiguration;
70 }
71
72 /**
73 * Convert the package configuration into a dependency definition
74 *
75 * This converts "dependencies" and "suggestions" to "after" syntax for the usage in DependencyOrderingService
76 *
77 * @param array $packageStatesConfiguration
78 * @param array $packageKeys
79 * @return array
80 * @throws \UnexpectedValueException
81 */
82 protected function convertConfigurationForGraph(array $packageStatesConfiguration, array $packageKeys) {
83 $dependencies = [];
84 foreach ($packageKeys as $packageKey) {
85 if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions']) ) {
86 continue;
87 }
88 $dependencies[$packageKey] = [
89 'after' => []
90 ];
91 if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
92 foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
93 if (!in_array($dependentPackageKey, $packageKeys, TRUE)) {
94 throw new \UnexpectedValueException(
95 'The package "' . $packageKey . '" depends on "'
96 . $dependentPackageKey . '" which is not present in the system.',
97 1382276561);
98 }
99 $dependencies[$packageKey]['after'][] = $dependentPackageKey;
100 }
101 }
102 if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
103 foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
104 // skip suggestions on not existing packages
105 if (in_array($suggestedPackageKey, $packageKeys, TRUE)) {
106 // Suggestions actually have never been meant to influence loading order.
107 // We misuse this currently, as there is no other way to influence the loading order
108 // for not-required packages (soft-dependency).
109 // When considering suggestions for the loading order, we might create a cyclic dependency
110 // if the suggested package already has a real dependency on this package, so the suggestion
111 // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
112 $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
113 }
114 }
115 }
116 }
117 return $dependencies;
118 }
119
120 /**
121 * Adds all root packages of current dependency graph as dependency to all extensions
122 *
123 * This ensures that the framework extensions (aka sysext) are
124 * always loaded first, before any other external extension.
125 *
126 * @param array $packageStateConfiguration
127 * @param array $rootPackageKeys
128 * @return array
129 */
130 protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys) {
131 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
132 $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
133 foreach ($extensionPackageKeys as $packageKey) {
134 // Remove framework packages from list
135 $packageKeysWithoutFramework = array_diff(
136 $packageStateConfiguration[$packageKey]['dependencies'],
137 $frameworkPackageKeys
138 );
139 // The order of the array_merge is crucial here,
140 // we want the framework first
141 $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
142 $rootPackageKeys, $packageKeysWithoutFramework
143 );
144 }
145 return $packageStateConfiguration;
146 }
147
148 /**
149 * Builds the dependency graph for all packages
150 *
151 * This method also introduces dependencies among the dependencies
152 * to ensure the loading order is exactly as specified in the list.
153 *
154 * @param array $packageStateConfiguration
155 * @return array
156 */
157 protected function buildDependencyGraph(array $packageStateConfiguration) {
158 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
159 $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
160 $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
161
162 $packageKeys = array_keys($packageStateConfiguration);
163 return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
164 }
165
166 /**
167 * @param array $packageStateConfiguration
168 * @return array
169 * @throws \TYPO3\CMS\Core\Exception
170 */
171 protected function findFrameworkPackages(array $packageStateConfiguration) {
172 $frameworkPackageKeys = array();
173 /** @var PackageManager $packageManager */
174 $packageManager = Bootstrap::getInstance()->getEarlyInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
175 foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
176 /** @var Package $package */
177 $package = $packageManager->getPackage($packageKey);
178 if (
179 $package instanceof Package
180 && (
181 $package->isPartOfFactoryDefault()
182 || $package->isPartOfMinimalUsableSystem()
183 || strpos($packageConfiguration['packagePath'], self::SYSEXT_FOLDER) === 0
184 )
185 ) {
186 $frameworkPackageKeys[] = $packageKey;
187 }
188 }
189
190 return $frameworkPackageKeys;
191 }
192
193 }