fec619460c31b4c4dbc815350459d7c704101a99
[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 class DependencyResolver
26 {
27 /**
28 * Folder with framework extensions
29 */
30 const SYSEXT_FOLDER = 'typo3/sysext';
31
32 /**
33 * @var DependencyOrderingService
34 */
35 protected $dependencyOrderingService;
36
37 /**
38 * @param DependencyOrderingService $dependencyOrderingService
39 */
40 public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService)
41 {
42 $this->dependencyOrderingService = $dependencyOrderingService;
43 }
44
45 /**
46 * @param array $packageStatesConfiguration
47 * @return array Returns the packageStatesConfiguration sorted by dependencies
48 * @throws \UnexpectedValueException
49 */
50 public function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration)
51 {
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 {
84 $dependencies = [];
85 foreach ($packageKeys as $packageKey) {
86 if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
87 continue;
88 }
89 $dependencies[$packageKey] = [
90 'after' => []
91 ];
92 if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
93 foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
94 if (!in_array($dependentPackageKey, $packageKeys, true)) {
95 throw new \UnexpectedValueException(
96 'The package "' . $packageKey . '" depends on "'
97 . $dependentPackageKey . '" which is not present in the system.',
98 1382276561);
99 }
100 $dependencies[$packageKey]['after'][] = $dependentPackageKey;
101 }
102 }
103 if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
104 foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
105 // skip suggestions on not existing packages
106 if (in_array($suggestedPackageKey, $packageKeys, true)) {
107 // Suggestions actually have never been meant to influence loading order.
108 // We misuse this currently, as there is no other way to influence the loading order
109 // for not-required packages (soft-dependency).
110 // When considering suggestions for the loading order, we might create a cyclic dependency
111 // if the suggested package already has a real dependency on this package, so the suggestion
112 // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
113 $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
114 }
115 }
116 }
117 }
118 return $dependencies;
119 }
120
121 /**
122 * Adds all root packages of current dependency graph as dependency to all extensions
123 *
124 * This ensures that the framework extensions (aka sysext) are
125 * always loaded first, before any other external extension.
126 *
127 * @param array $packageStateConfiguration
128 * @param array $rootPackageKeys
129 * @return array
130 */
131 protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
132 {
133 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
134 $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
135 foreach ($extensionPackageKeys as $packageKey) {
136 // Remove framework packages from list
137 $packageKeysWithoutFramework = array_diff(
138 $packageStateConfiguration[$packageKey]['dependencies'],
139 $frameworkPackageKeys
140 );
141 // The order of the array_merge is crucial here,
142 // we want the framework first
143 $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
144 $rootPackageKeys, $packageKeysWithoutFramework
145 );
146 }
147 return $packageStateConfiguration;
148 }
149
150 /**
151 * Builds the dependency graph for all packages
152 *
153 * This method also introduces dependencies among the dependencies
154 * to ensure the loading order is exactly as specified in the list.
155 *
156 * @param array $packageStateConfiguration
157 * @return array
158 */
159 protected function buildDependencyGraph(array $packageStateConfiguration)
160 {
161 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
162 $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
163 $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
164
165 $packageKeys = array_keys($packageStateConfiguration);
166 return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
167 }
168
169 /**
170 * @param array $packageStateConfiguration
171 * @return array
172 * @throws \TYPO3\CMS\Core\Exception
173 */
174 protected function findFrameworkPackages(array $packageStateConfiguration)
175 {
176 $frameworkPackageKeys = array();
177 /** @var PackageManager $packageManager */
178 $packageManager = Bootstrap::getInstance()->getEarlyInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
179 foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
180 /** @var Package $package */
181 $package = $packageManager->getPackage($packageKey);
182 if ($package->getValueFromComposerManifest('type') === 'typo3-cms-framework') {
183 $frameworkPackageKeys[] = $packageKey;
184 }
185 }
186
187 return $frameworkPackageKeys;
188 }
189 }