[BUGFIX] Sorting of packages by dependency
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / PackageManager.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3
4 /* *
5 * This script belongs to the TYPO3 Flow framework. *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13
14 use TYPO3\Flow\Annotations as Flow;
15
16 /**
17 * The default TYPO3 Package Manager
18 * Adapted from FLOW for TYPO3 CMS
19 *
20 * @api
21 * @Flow\Scope("singleton")
22 */
23 class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO3\CMS\Core\SingletonInterface {
24
25
26 /**
27 * @var \TYPO3\CMS\Core\Core\ClassLoader
28 */
29 protected $classLoader;
30
31 /**
32 * @var \TYPO3\CMS\Core\Core\Bootstrap
33 */
34 protected $bootstrap;
35
36 /**
37 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
38 */
39 protected $coreCache;
40
41 /**
42 * @var string
43 */
44 protected $cacheIdentifier;
45
46 /**
47 * @var array
48 */
49 protected $extAutoloadClassFiles;
50
51 /**
52 * @var array
53 */
54 protected $packagesBasePaths = array();
55
56 /**
57 * @var array
58 */
59 protected $packageAliasMap = array();
60
61 /**
62 * Adjacency matrix for the dependency graph (DAG)
63 *
64 * Example structure is:
65 * A => (A => FALSE, B => TRUE, C => FALSE)
66 * B => (A => FALSE, B => FALSE, C => FALSE)
67 * C => (A => TRUE, B => FALSE, C => FALSE)
68 *
69 * A depends on B, C depends on A, B is independent
70 *
71 * @var array<array<boolean>>
72 */
73 protected $dependencyGraph;
74
75 /**
76 * Constructor
77 */
78 public function __construct() {
79 $this->packagesBasePaths = array(
80 'local' => PATH_typo3conf . 'ext',
81 'global' => PATH_typo3 . 'ext',
82 'sysext' => PATH_typo3 . 'sysext',
83 'composer' => PATH_site . 'Packages',
84 );
85 }
86
87 /**
88 * @param \TYPO3\CMS\Core\Core\ClassLoader $classLoader
89 */
90 public function injectClassLoader(\TYPO3\CMS\Core\Core\ClassLoader $classLoader) {
91 $this->classLoader = $classLoader;
92 }
93
94 /**
95 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
96 */
97 public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache) {
98 $this->coreCache = $coreCache;
99 }
100
101 /**
102 * Initializes the package manager
103 *
104 * @param \TYPO3\CMS\Core\Core\Bootstrap $bootstrap The current bootstrap; Flow Bootstrap is here by intention to keep the PackageManager valid to the interface
105 * @param string $packagesBasePath Absolute path of the Packages directory
106 * @param string $packageStatesPathAndFilename
107 * @return void
108 */
109 public function initialize(\TYPO3\Flow\Core\Bootstrap $bootstrap, $packagesBasePath = PATH_site, $packageStatesPathAndFilename = '') {
110
111 $this->bootstrap = $bootstrap;
112 $this->packagesBasePath = $packagesBasePath;
113 $this->packageStatesPathAndFilename = ($packageStatesPathAndFilename === '') ? PATH_typo3conf . 'PackageStates.php' : $packageStatesPathAndFilename;
114 $this->packageFactory = new PackageFactory($this);
115
116 $this->loadPackageStates();
117
118 $requiredList = array();
119 foreach ($this->packages as $packageKey => $package) {
120 $protected = $package->isProtected();
121 if ($protected) {
122 $requiredList[$packageKey] = $package;
123 }
124 if (isset($this->packageStatesConfiguration['packages'][$packageKey]['state']) && $this->packageStatesConfiguration['packages'][$packageKey]['state'] === 'active') {
125 $this->activePackages[$packageKey] = $package;
126 }
127 }
128 $previousActivePackage = $this->activePackages;
129 $this->activePackages = array_merge($requiredList, $this->activePackages);
130
131 if ($this->activePackages != $previousActivePackage) {
132 foreach ($requiredList as $requiredPackageKey => $package) {
133 $this->packageStatesConfiguration['packages'][$requiredPackageKey]['state'] = 'active';
134 }
135 $this->sortAndSavePackageStates();
136 }
137
138 //@deprecated since 6.2, don't use
139 if (!defined('REQUIRED_EXTENSIONS')) {
140 // List of extensions required to run the core
141 define('REQUIRED_EXTENSIONS', implode(',', array_keys($requiredList)));
142 }
143
144 $cacheIdentifier = $this->getCacheIdentifier();
145 if ($cacheIdentifier === NULL) {
146 // Create an artificial cache identifier if the package states file is not available yet
147 // in order that the class loader and class alias map can cache anyways.
148 $cacheIdentifier = substr(md5(implode('###', array_keys($this->activePackages))), 0, 13);
149 }
150 $this->classLoader->setCacheIdentifier($cacheIdentifier)->setPackages($this->activePackages);
151
152 foreach ($this->activePackages as $package) {
153 $package->boot($bootstrap);
154 }
155
156 $this->saveToPackageCache();
157 }
158
159 /**
160 * @return string
161 */
162 protected function getCacheIdentifier() {
163 if ($this->cacheIdentifier === NULL) {
164 if (@file_exists($this->packageStatesPathAndFilename)) {
165 $this->cacheIdentifier = substr(md5_file($this->packageStatesPathAndFilename), 0, 13);
166 } else {
167 $this->cacheIdentifier = NULL;
168 }
169 }
170 return $this->cacheIdentifier;
171 }
172
173 /**
174 * @return string
175 */
176 protected function getCacheEntryIdentifier() {
177 $cacheIdentifier = $this->getCacheIdentifier();
178 return $cacheIdentifier !== NULL ? 'PackageManager_' . $cacheIdentifier : NULL;
179 }
180
181 /**
182 *
183 */
184 protected function saveToPackageCache() {
185 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
186 if ($cacheEntryIdentifier !== NULL && !$this->coreCache->has($cacheEntryIdentifier)) {
187 $cacheEntryPath = rtrim(\TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath($this->coreCache->getBackend()->getCacheDirectory()), '/');
188 // Package objects get their own cache entry, so PHP does not have to parse the serialized string
189 $packageObjectsCacheEntryIdentifier = uniqid('PackageObjects_');
190 // Build cache file
191 $packageCache = array(
192 'packageStatesConfiguration' => $this->packageStatesConfiguration,
193 'packageAliasMap' => $this->packageAliasMap,
194 'packageKeys' => $this->packageKeys,
195 'declaringPackageClassPathsAndFilenames' => array(),
196 'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
197 );
198 foreach ($this->packages as $package) {
199 if (!isset($packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName = get_class($package)])) {
200 $reflectionPackageClass = new \ReflectionClass($packageClassName);
201 $packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName] = $reflectionPackageClass->getFileName();
202 }
203 }
204 $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
205 $this->coreCache->set(
206 $cacheEntryIdentifier,
207 'return __DIR__ !== \'' . $cacheEntryPath . '\' ? FALSE : ' . PHP_EOL .
208 var_export($packageCache, TRUE) . ';'
209 );
210 }
211 }
212
213 /**
214 * Loads the states of available packages from the PackageStates.php file.
215 * The result is stored in $this->packageStatesConfiguration.
216 *
217 * @return void
218 */
219 protected function loadPackageStates() {
220 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
221 if ($cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier) && $packageCache = $this->coreCache->requireOnce($cacheEntryIdentifier)) {
222 foreach ($packageCache['declaringPackageClassPathsAndFilenames'] as $packageClassPathAndFilename) {
223 require_once $packageClassPathAndFilename;
224 }
225 $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
226 $this->packageAliasMap = $packageCache['packageAliasMap'];
227 $this->packageKeys = $packageCache['packageKeys'];
228 $GLOBALS['TYPO3_currentPackageManager'] = $this;
229 // Strip off PHP Tags from Php Cache Frontend
230 $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
231 $this->packages = unserialize($packageObjects);
232 unset($GLOBALS['TYPO3_currentPackageManager']);
233 } else {
234 $this->packageStatesConfiguration = @include($this->packageStatesPathAndFilename) ?: array();
235 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
236 $this->packageStatesConfiguration = array();
237 }
238 if ($this->packageStatesConfiguration !== array()) {
239 $this->registerPackagesFromConfiguration();
240 } else {
241 throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
242 }
243 }
244 }
245
246
247 /**
248 * Scans all directories in the packages directories for available packages.
249 * For each package a Package object is created and stored in $this->packages.
250 *
251 * @return void
252 * @throws \TYPO3\Flow\Package\Exception\DuplicatePackageException
253 */
254 public function scanAvailablePackages() {
255 $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
256
257 if (isset($this->packageStatesConfiguration['packages'])) {
258 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
259 if (!@file_exists($this->packagesBasePath . $configuration['packagePath'])) {
260 unset($this->packageStatesConfiguration['packages'][$packageKey]);
261 }
262 }
263 } else {
264 $this->packageStatesConfiguration['packages'] = array();
265 }
266
267 foreach ($this->packagesBasePaths as $key => $packagesBasePath) {
268 if (!is_dir($packagesBasePath)) {
269 unset($this->packagesBasePaths[$key]);
270 }
271 }
272
273 $packagePaths = $this->scanLegacyExtensions();
274 foreach ($this->packagesBasePaths as $packagesBasePath) {
275 $this->scanPackagesInPath($packagesBasePath, $packagePaths);
276 }
277
278 foreach ($packagePaths as $packagePath => $composerManifestPath) {
279 $packagesBasePath = PATH_site;
280 foreach ($this->packagesBasePaths as $basePath) {
281 if (strpos($packagePath, $basePath) === 0) {
282 $packagesBasePath = $basePath;
283 break;
284 }
285 }
286 try {
287 $composerManifest = self::getComposerManifest($composerManifestPath);
288 $packageKey = \TYPO3\CMS\Core\Package\PackageFactory::getPackageKeyFromManifest($composerManifest, $packagePath, $packagesBasePath);
289 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
290 $this->packageStatesConfiguration['packages'][$packageKey]['manifestPath'] = substr($composerManifestPath, strlen($packagePath)) ? : '';
291 $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
292 } catch (\TYPO3\Flow\Package\Exception\MissingPackageManifestException $exception) {
293 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
294 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
295 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
296 continue;
297 }
298 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
299 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
300 }
301
302 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
303
304 // Change this to read the target from Composer or any other source
305 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = \TYPO3\Flow\Package\Package::DIRECTORY_CLASSES;
306 }
307
308 $registerOnlyNewPackages = !empty($this->packages);
309 $this->registerPackagesFromConfiguration($registerOnlyNewPackages);
310 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
311 $this->sortAndsavePackageStates();
312 }
313 }
314
315 /**
316 * @return array
317 */
318 protected function scanLegacyExtensions(&$collectedExtensionPaths = array()) {
319 $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
320 foreach ($this->packagesBasePaths as $type => $packageBasePath) {
321 if (!in_array($type, $legacyCmsPackageBasePathTypes)) {
322 continue;
323 }
324 /** @var $fileInfo \SplFileInfo */
325 foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
326 if (!$fileInfo->isDir()) {
327 continue;
328 }
329 $filename = $fileInfo->getFilename();
330 if ($filename[0] !== '.') {
331 $currentPath = \TYPO3\Flow\Utility\Files::getUnixStylePath($fileInfo->getPathName()) . '/';
332 if (file_exists($currentPath . 'ext_emconf.php')) {
333 $collectedExtensionPaths[$currentPath] = $currentPath;
334 }
335 }
336 }
337 }
338 return $collectedExtensionPaths;
339 }
340
341 /**
342 * Looks for composer.json in the given path and returns a path or NULL.
343 *
344 * @param string $packagePath
345 * @return array
346 */
347 protected function findComposerManifestPaths($packagePath) {
348 // If an ext_emconf.php file is found, we don't need to look deeper
349 if (file_exists($packagePath . '/ext_emconf.php')) {
350 return array();
351 }
352 return parent::findComposerManifestPaths($packagePath);
353 }
354
355 /**
356 * Requires and registers all packages which were defined in packageStatesConfiguration
357 *
358 * @param boolean $registerOnlyNewPackages
359 * @return void
360 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
361 */
362 protected function registerPackagesFromConfiguration($registerOnlyNewPackages = FALSE) {
363 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
364
365 if ($registerOnlyNewPackages && $this->isPackageAvailable($packageKey)) {
366 continue;
367 }
368
369 $packagePath = isset($stateConfiguration['packagePath']) ? $stateConfiguration['packagePath'] : NULL;
370 $classesPath = isset($stateConfiguration['classesPath']) ? $stateConfiguration['classesPath'] : NULL;
371 $manifestPath = isset($stateConfiguration['manifestPath']) ? $stateConfiguration['manifestPath'] : NULL;
372
373 try {
374 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath);
375 } catch (\TYPO3\Flow\Package\Exception\InvalidPackagePathException $exception) {
376 $this->unregisterPackageByPackageKey($packageKey);
377 continue;
378 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
379 $this->unregisterPackageByPackageKey($packageKey);
380 continue;
381 }
382
383 $this->registerPackage($package, FALSE);
384
385 if (!$this->packages[$packageKey] instanceof \TYPO3\Flow\Package\PackageInterface) {
386 throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782487);
387 }
388
389 $this->packageKeys[strtolower($packageKey)] = $packageKey;
390 if ($stateConfiguration['state'] === 'active') {
391 $this->activePackages[$packageKey] = $this->packages[$packageKey];
392 }
393 }
394 }
395
396 /**
397 * Register a native Flow package
398 *
399 * @param string $packageKey The Package to be registered
400 * @param boolean $sortAndSave allows for not saving packagestates when used in loops etc.
401 * @return \TYPO3\Flow\Package\PackageInterface
402 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
403 */
404 public function registerPackage(\TYPO3\Flow\Package\PackageInterface $package, $sortAndSave = TRUE) {
405 $package = parent::registerPackage($package, $sortAndSave);
406 if ($package instanceof PackageInterface) {
407 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
408 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
409 }
410 }
411 return $package;
412 }
413
414 /**
415 * Unregisters a package from the list of available packages
416 *
417 * @param string $packageKey Package Key of the package to be unregistered
418 * @return void
419 */
420 protected function unregisterPackageByPackageKey($packageKey) {
421 try {
422 $package = $this->getPackage($packageKey);
423 if ($package instanceof PackageInterface) {
424 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
425 unset($this->packageAliasMap[strtolower($packageToReplace)]);
426 }
427 $packageKey = $package->getPackageKey();
428 }
429 } catch (\TYPO3\Flow\Package\Exception\UnknownPackageException $e) {
430 }
431 parent::unregisterPackageByPackageKey($packageKey);
432 }
433
434 /**
435 * Resolves a Flow package key from a composer package name.
436 *
437 * @param string $composerName
438 * @return string
439 * @throws \TYPO3\Flow\Package\Exception\InvalidPackageStateException
440 */
441 public function getPackageKeyFromComposerName($composerName) {
442 if (isset($this->packageAliasMap[$composerName])) {
443 return $this->packageAliasMap[$composerName];
444 }
445 try {
446 return parent::getPackageKeyFromComposerName($composerName);
447 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageStateException $exception) {
448 return $composerName;
449 }
450 }
451
452 /**
453 * @return array
454 */
455 public function getExtAutoloadRegistry() {
456 if (!isset($this->extAutoloadClassFiles)) {
457 $classRegistry = array();
458 foreach ($this->activePackages as $packageKey => $packageData) {
459 try {
460 $extensionAutoloadFile = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($packageKey, 'ext_autoload.php');
461 if (@file_exists($extensionAutoloadFile)) {
462 $classRegistry = array_merge($classRegistry, require $extensionAutoloadFile);
463 }
464 } catch (\BadFunctionCallException $e) {
465 }
466 }
467 $this->extAutoloadClassFiles = $classRegistry;
468 }
469 return $this->extAutoloadClassFiles;
470 }
471
472 /**
473 * Returns a PackageInterface object for the specified package.
474 * A package is available, if the package directory contains valid MetaData information.
475 *
476 * @param string $packageKey
477 * @return \TYPO3\Flow\Package\PackageInterface The requested package object
478 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
479 * @api
480 */
481 public function getPackage($packageKey) {
482 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
483 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
484 }
485 return parent::getPackage($packageKey);
486 }
487
488 /**
489 * Returns TRUE if a package is available (the package's files exist in the packages directory)
490 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
491 *
492 * @param string $packageKey The key of the package to check
493 * @return boolean TRUE if the package is available, otherwise FALSE
494 * @api
495 */
496 public function isPackageAvailable($packageKey) {
497 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
498 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
499 }
500 return parent::isPackageAvailable($packageKey);
501 }
502
503 /**
504 * Returns TRUE if a package is activated or FALSE if it's not.
505 *
506 * @param string $packageKey The key of the package to check
507 * @return boolean TRUE if package is active, otherwise FALSE
508 * @api
509 */
510 public function isPackageActive($packageKey) {
511 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
512 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
513 }
514 return parent::isPackageActive($packageKey);
515 }
516
517 /**
518 * @param string $packageKey
519 */
520 public function deactivatePackage($packageKey) {
521 $package = $this->getPackage($packageKey);
522 parent::deactivatePackage($package->getPackageKey());
523 }
524
525 /**
526 * @param string $packageKey
527 */
528 public function activatePackage($packageKey) {
529 $package = $this->getPackage($packageKey);
530 parent::activatePackage($package->getPackageKey());
531 }
532
533
534 /**
535 * @param string $packageKey
536 */
537 public function deletePackage($packageKey) {
538 $package = $this->getPackage($packageKey);
539 parent::deletePackage($package->getPackageKey());
540 }
541
542
543 /**
544 * @param string $packageKey
545 */
546 public function freezePackage($packageKey) {
547 $package = $this->getPackage($packageKey);
548 parent::freezePackage($package->getPackageKey());
549 }
550
551 /**
552 * @param string $packageKey
553 */
554 public function isPackageFrozen($packageKey) {
555 $package = $this->getPackage($packageKey);
556 parent::isPackageFrozen($package->getPackageKey());
557 }
558
559 /**
560 * @param string $packageKey
561 */
562 public function unfreezePackage($packageKey) {
563 $package = $this->getPackage($packageKey);
564 parent::unfreezePackage($package->getPackageKey());
565 }
566
567 /**
568 * @param string $packageKey
569 */
570 public function refreezePackage($packageKey) {
571 $package = $this->getPackage($packageKey);
572 parent::refreezePackage($package->getPackageKey());
573 }
574
575
576 /**
577 * Get packages of specific type
578 *
579 * @param string $type Type of package. Empty string for all types
580 * @param array $excludedTypes Array of package types to exclude
581 * @return array List of packages
582 */
583 protected function getPackageKeysOfType($type, array $excludedTypes = array()) {
584 $packageKeys = array();
585 foreach ($this->packages as $packageKey => $package) {
586 $packageType = $package->getComposerManifest('type');
587 if (($type === '' || $packageType === $type) && !in_array($packageType, $excludedTypes)) {
588 $packageKeys[] = $packageKey;
589 }
590 }
591 return $packageKeys;
592 }
593
594 /**
595 * Build the dependency graph for the given packages
596 *
597 * @param array $packageKeys
598 * @return void
599 * @throws \UnexpectedValueException
600 */
601 protected function buildDependencyGraphForPackages(array $packageKeys) {
602 // Initialize the dependencies with FALSE
603 $this->dependencyGraph = array_fill_keys($packageKeys, array_fill_keys($packageKeys, FALSE));
604 foreach ($packageKeys as $packageKey) {
605 $dependentPackageKeys = $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'];
606 foreach ($dependentPackageKeys as $dependentPackageKey) {
607 if (!in_array($dependentPackageKey, $packageKeys)) {
608 throw new \UnexpectedValueException(
609 'The package "' . $packageKey .'" depends on "'
610 . $dependentPackageKey . '" which is not present in the system.',
611 1382276561);
612 }
613 $this->dependencyGraph[$packageKey][$dependentPackageKey] = TRUE;
614 }
615 }
616 }
617
618 /**
619 * Adds all root packages of current dependency graph as dependency
620 * to all extensions
621 *
622 * @return void
623 */
624 protected function addDependencyToFrameworkToAllExtensions() {
625 $rootPackageKeys = array();
626 foreach (array_keys($this->dependencyGraph) as $packageKey) {
627 if (!$this->getIncomingEdgeCount($packageKey)) {
628 $rootPackageKeys[] = $packageKey;
629 }
630 }
631 $extensionPackageKeys = $this->getPackageKeysOfType('', array('typo3-cms-framework'));
632 $frameworkPackageKeys = $this->getPackageKeysOfType('typo3-cms-framework');
633 foreach ($extensionPackageKeys as $packageKey) {
634 // Remove framework packages from list
635 $packageKeysWithoutFramework = array_diff(
636 $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'],
637 $frameworkPackageKeys
638 );
639 // The order of the array_merge is crucial here,
640 // we want the framework first
641 $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = array_merge(
642 $rootPackageKeys, $packageKeysWithoutFramework
643 );
644 }
645 }
646
647 /**
648 * Builds the dependency graph for all packages
649 *
650 * This method also introduces dependencies among the dependencies
651 * to ensure the loading order is exactly as specified in the list.
652 *
653 * @return void
654 */
655 protected function buildDependencyGraph() {
656 $this->resolvePackageDependencies();
657
658 $frameworkPackageKeys = $this->getPackageKeysOfType('typo3-cms-framework');
659 $this->buildDependencyGraphForPackages($frameworkPackageKeys);
660
661 $this->addDependencyToFrameworkToAllExtensions();
662
663 $packageKeys = array_keys($this->packages);
664 $this->buildDependencyGraphForPackages($packageKeys);
665 }
666
667 /**
668 * Get the number of incoming edges in the dependency graph
669 * for given package key.
670 *
671 * @param string $packageKey
672 * @return integer
673 */
674 protected function getIncomingEdgeCount($packageKey) {
675 $incomingEdgeCount = 0;
676 foreach ($this->dependencyGraph as $dependencies) {
677 if ($dependencies[$packageKey]) {
678 $incomingEdgeCount++;
679 }
680 }
681 return $incomingEdgeCount;
682 }
683
684 /**
685 * Get the loading order for packages
686 *
687 * @return array The properly sorted loading order
688 * @throws \UnexpectedValueException
689 */
690 protected function getAvailablePackageLoadingOrder() {
691 $this->buildDependencyGraph();
692
693 // This will contain our final result
694 $sortedPackageKeys = array();
695
696 $rootPackageKeys = array();
697 // Filter extensions with no incoming edge
698 foreach (array_keys($this->dependencyGraph) as $packageKey) {
699 if (!$this->getIncomingEdgeCount($packageKey)) {
700 $rootPackageKeys[] = $packageKey;
701 }
702 }
703
704 while (count($rootPackageKeys)) {
705 $currentPackageKey = array_shift($rootPackageKeys);
706 array_push($sortedPackageKeys, $currentPackageKey);
707
708 $dependingPackageKeys = array_keys(array_filter($this->dependencyGraph[$currentPackageKey]));
709 foreach ($dependingPackageKeys as $dependingPackageKey) {
710 // Remove the edge to this dependency
711 $this->dependencyGraph[$currentPackageKey][$dependingPackageKey] = FALSE;
712 if (!$this->getIncomingEdgeCount($dependingPackageKey)) {
713 // We found a new root, lets add it
714 array_unshift($rootPackageKeys, $dependingPackageKey);
715 }
716 }
717 }
718
719 // Check for remaining edges in the graph
720 $cycles = array();
721 array_walk($this->dependencyGraph, function($dependencies, $packageKeyFrom) use(&$cycles) {
722 array_walk($dependencies, function($dependency, $packageKeyTo) use(&$cycles, $packageKeyFrom) {
723 if ($dependency) {
724 $cycles[] = $packageKeyFrom . '->' . $packageKeyTo;
725 }
726 });
727 });
728 if (count($cycles)) {
729 throw new \UnexpectedValueException('Your dependencies have cycles. That will not work out. Cycles found: ' . implode(', ', $cycles), 1381960493);
730 }
731
732 // We built now a list of dependencies
733 // Reverse the list to get the correct loading order
734 return array_reverse($sortedPackageKeys);
735 }
736
737 /**
738 * Orders all packages by comparing their dependencies. By this, the packages
739 * and package configurations arrays holds all packages in the correct
740 * initialization order.
741 *
742 * @return void
743 */
744 protected function sortAvailablePackagesByDependencies() {
745 $newPackages = array();
746 $newPackageStatesConfiguration = array();
747
748 $sortedPackageKeys = $this->getAvailablePackageLoadingOrder();
749
750 // Reorder the packages according to the loading order
751 foreach ($sortedPackageKeys as $packageKey) {
752 $newPackages[$packageKey] = $this->packages[$packageKey];
753 $newPackageStatesConfiguration[$packageKey] = $this->packageStatesConfiguration['packages'][$packageKey];
754 }
755
756 $this->packages = $newPackages;
757 $this->packageStatesConfiguration['packages'] = $newPackageStatesConfiguration;
758 }
759 }