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