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