[FEATURE] Allow the activation of packages during runtime 39/24939/5
authorThomas Maroschik <tmaroschik@dfau.de>
Mon, 21 Oct 2013 17:10:42 +0000 (19:10 +0200)
committerMarkus Klein <klein.t3@mfc-linz.at>
Wed, 18 Dec 2013 20:05:23 +0000 (21:05 +0100)
Due to the nature of the Flow Package Manager, packages cannot
be activated and directly used during runtime. Before the Package
Manager it was possible to activate/deactivate extensions in
AdditionalConfiguration.php under certain custom conditions.

This patch introduces a new setting in $GLOBALS['TYPO3_CONF_VARS']
['EXT']['runtimeActivatedPackages'] = array('{packageKey}') that
gets initialized right after the package management initialization.

Resolves: #53015
Releases: 6.2
Change-Id: Id3b85a3feb00876d2a04a02e85450a4568eb5bff
Reviewed-on: https://review.typo3.org/24939
Reviewed-by: Thomas Maroschik
Reviewed-by: Stefan Neufeind
Tested-by: Stefan Neufeind
Tested-by: Alexander Stehlik
Tested-by: Frans Saris
Reviewed-by: Stefan Froemken
Tested-by: Stefan Froemken
Reviewed-by: Markus Klein
Tested-by: Markus Klein
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/Core/ClassAliasMap.php
typo3/sysext/core/Classes/Core/ClassLoader.php
typo3/sysext/core/Classes/Package/Package.php
typo3/sysext/core/Classes/Package/PackageInterface.php
typo3/sysext/core/Classes/Package/PackageManager.php

index 6c03a21..5498ebc 100644 (file)
@@ -227,7 +227,8 @@ class Bootstrap {
                        ->populateLocalConfiguration()
                        ->initializeCachingFramework()
                        ->initializeClassLoaderCaches()
-                       ->initializePackageManagement($packageManagerClassName);
+                       ->initializePackageManagement($packageManagerClassName)
+                       ->initializeRuntimeActivatedPackagesFromConfiguration();
 
                // @TODO dig into this
                if (!$allowCaching) {
@@ -261,7 +262,7 @@ class Bootstrap {
        protected function initializeClassLoader() {
                $classLoader = new ClassLoader($this->applicationContext);
                $this->setEarlyInstance('TYPO3\\CMS\\Core\\Core\\ClassLoader', $classLoader);
-               $classLoader->setEarlyClassInformationsFromAutoloadRegistry((array) include __DIR__ . '/../../ext_autoload.php');
+               $classLoader->setRuntimeClassLoadingInformationFromAutoloadRegistry((array) include __DIR__ . '/../../ext_autoload.php');
                $classAliasMap = new \TYPO3\CMS\Core\Core\ClassAliasMap();
                $classAliasMap->injectClassLoader($classLoader);
                $this->setEarlyInstance('TYPO3\\CMS\\Core\\Core\\ClassAliasMap', $classAliasMap);
@@ -322,6 +323,23 @@ class Bootstrap {
                return $this;
        }
 
+       /**
+        * Activates a package during runtime. This is used in AdditionalConfiguration.php
+        * to enable extensions under conditions.
+        *
+        * @return Bootstrap
+        */
+       protected function initializeRuntimeActivatedPackagesFromConfiguration() {
+               if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages'])) {
+                       /** @var \TYPO3\CMS\Core\Package\PackageManager $packageManager */
+                       $packageManager = $this->getEarlyInstance('TYPO3\\Flow\\Package\\PackageManager');
+                       foreach ($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages'] as $runtimeAddedPackageKey) {
+                               $packageManager->activatePackageDuringRuntime($runtimeAddedPackageKey);
+                       }
+               }
+               return $this;
+       }
+
        /**
         * Load ext_localconf of extensions
         *
index cb8cb2b..9f2876c 100644 (file)
@@ -123,24 +123,11 @@ class ClassAliasMap implements \TYPO3\CMS\Core\SingletonInterface {
         * Set packages
         *
         * @param array $packages
+        * @return ClassAliasMap
         */
        public function setPackages(array $packages) {
                $this->packages = $packages;
-               if (!$this->loadEarlyInstanceMappingFromCache()) {
-                       $classNameToAliasMapping = $this->buildMappingAndInitializeEarlyInstanceMapping();
-                       $this->buildMappingFiles($classNameToAliasMapping);
-               }
-       }
-
-       /**
-        * Return class name to alias mapping
-        *
-        * @param array $packages
-        * @return array
-        */
-       public function setPackagesButDontBuildMappingFilesReturnClassNameToAliasMappingInstead(array $packages) {
-               $this->packages = $packages;
-               return $this->buildMappingAndInitializeEarlyInstanceMapping();
+               return $this;
        }
 
        /**
@@ -148,10 +135,10 @@ class ClassAliasMap implements \TYPO3\CMS\Core\SingletonInterface {
         *
         * @return boolean
         */
-       protected function loadEarlyInstanceMappingFromCache() {
+       public function loadEarlyInstanceMappingFromCache() {
                $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
                if (!$cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier)) {
-                       return (bool$this->coreCache->requireOnce($cacheEntryIdentifier);
+                       return (boolean)$this->coreCache->requireOnce($cacheEntryIdentifier);
                }
                return FALSE;
        }
@@ -161,7 +148,7 @@ class ClassAliasMap implements \TYPO3\CMS\Core\SingletonInterface {
         *
         * @return array
         */
-       protected function buildMappingAndInitializeEarlyInstanceMapping() {
+       public function buildMappingAndInitializeEarlyInstanceMapping() {
                $aliasToClassNameMapping = array();
                foreach ($this->packages as $package) {
                        if ($package instanceof \TYPO3\CMS\Core\Package\Package) {
index c29aa33..846e467 100644 (file)
@@ -27,8 +27,9 @@ namespace TYPO3\CMS\Core\Core;
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
-use TYPO3\CMS\Core\Package\Package;
+use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Cache;
 
 /**
  * Class Loader implementation which loads .php files found in the classes
@@ -74,22 +75,25 @@ class ClassLoader {
        /**
         * @var array
         */
-       protected $earlyClassInformationsFromAutoloadRegistry = array();
+       protected $runtimeClassLoadingInformationCache = array();
 
        /**
         * @var array A list of namespaces this class loader is definitely responsible for
         */
-       protected $packageNamespaces = array(
-               'TYPO3\CMS\Core' => 14
-       );
+       protected $packageNamespaces = array();
 
        /**
         * @var array A list of packages and their replaces pointing to class paths
         */
        protected $packageClassesPaths = array();
 
+       /**
+        * Constructor
+        *
+        * @param ApplicationContext $context
+        */
        public function __construct(ApplicationContext $context) {
-               $this->classesCache = new \TYPO3\CMS\Core\Cache\Frontend\StringFrontend('cache_classes', new \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend($context));
+               $this->classesCache = new Cache\Frontend\StringFrontend('cache_classes', new Cache\Backend\TransientMemoryBackend($context));
        }
 
        /**
@@ -103,9 +107,11 @@ class ClassLoader {
        }
 
        /**
+        * Get core cache injected
+        *
         * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
         */
-       public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache) {
+       public function injectCoreCache(Cache\Frontend\PhpFrontend $coreCache) {
                $this->coreCache = $coreCache;
                $this->classAliasMap->injectCoreCache($coreCache);
        }
@@ -115,8 +121,8 @@ class ClassLoader {
         *
         * @param \TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache
         */
-       public function injectClassesCache(\TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache) {
-               /** @var $earlyClassLoaderBackend \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend */
+       public function injectClassesCache(Cache\Frontend\StringFrontend $classesCache) {
+               /** @var $earlyClassLoaderBackend Cache\Backend\TransientMemoryBackend */
                $earlyClassesCache = $this->classesCache;
                $this->classesCache = $classesCache;
                $this->isEarlyCache = FALSE;
@@ -169,7 +175,7 @@ class ClassLoader {
 
                $loadingSuccessful = FALSE;
                if ($classLoadingInformation !== NULL) {
-                       $loadingSuccessful = (bool)require_once $classLoadingInformation[0];
+                       $loadingSuccessful = (boolean)require_once $classLoadingInformation[0];
                }
                if ($loadingSuccessful && count($classLoadingInformation) > 2) {
                        $originalClassName = $classLoadingInformation[1];
@@ -189,7 +195,7 @@ class ClassLoader {
                $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
 
                if ($classLoadingInformation === NULL) {
-                       $classLoadingInformation = $this->buildClassLoadingInformationForClassFromEarlyAutoloadRegistry($className);
+                       $classLoadingInformation = $this->fetchClassLoadingInformationFromRuntimeCache($className);
                }
 
                if ($classLoadingInformation === NULL) {
@@ -236,14 +242,13 @@ class ClassLoader {
         * @param string $className
         * @return array|null
         */
-       protected function buildClassLoadingInformationForClassFromEarlyAutoloadRegistry($className) {
+       protected function fetchClassLoadingInformationFromRuntimeCache($className) {
                $lowercasedClassName = strtolower($className);
-               if (isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName])) {
-                       if (@file_exists($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName][0])) {
-                               return $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName];
-                       }
+               if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
+                       return NULL;
                }
-               return NULL;
+               $classInformation = $this->runtimeClassLoadingInformationCache[$lowercasedClassName];
+               return @file_exists($classInformation[0]) ? $classInformation : NULL;
        }
 
        /**
@@ -305,8 +310,11 @@ class ClassLoader {
                        return NULL;
                }
 
-               if (isset($classNameParts[0]) && isset($classNameParts[1])
-                       && $classNameParts[0] === 'TYPO3' && $classNameParts[1] === 'CMS'
+               if (
+                               isset($classNameParts[0])
+                               && isset($classNameParts[1])
+                               && $classNameParts[0] === 'TYPO3'
+                               && $classNameParts[1] === 'CMS'
                ) {
                        $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
                        $classNameWithoutVendorAndProduct = $classNameParts[3];
@@ -374,20 +382,64 @@ class ClassLoader {
         */
        public function setPackages(array $packages) {
                $this->packages = $packages;
-               if (!$this->loadPackageNamespacesFromCache()) {
-                       $this->buildPackageNamespaces();
-                       $this->buildPackageClassesPathsForLegacyExtensions();
-                       $this->savePackageNamespacesAndClassesPathsToCache();
-                       // Rebuild the class alias map too because ext_autoload can contain aliases
-                       $classNameToAliasMapping = $this->classAliasMap->setPackagesButDontBuildMappingFilesReturnClassNameToAliasMappingInstead($packages);
-                       $this->buildClassInformationsFromFullAutoloadRegistry();
-                       $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
+               if (!$this->loadPackageNamespacesFromCache() || !$this->classAliasMap->loadEarlyInstanceMappingFromCache()) {
+                       $this->buildPackageNamespacesAndClassesPaths();
                } else {
                        $this->classAliasMap->setPackages($packages);
                }
+               // Clear the runtime cache for runtime activated packages
+               $this->runtimeClassLoadingInformationCache = array();
+               return $this;
+       }
+
+       /**
+        * Add a package to class loader just during runtime, so classes can be loaded without the need for a new request
+        *
+        * @param \TYPO3\Flow\Package\PackageInterface $package
+        * @return ClassLoader
+        */
+       public function addRuntimeActivatedPackage(\TYPO3\Flow\Package\PackageInterface $package) {
+               $this->packages[] = $package;
+               $this->buildPackageNamespaceAndClassesPath($package);
+               $this->sortPackageNamespaces();
+               $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array($package));
                return $this;
        }
 
+       /**
+        * Builds the package namespaces and classes paths for the given packages
+        *
+        * @return void
+        */
+       protected function buildPackageNamespacesAndClassesPaths() {
+               foreach ($this->packages as $package) {
+                       $this->buildPackageNamespaceAndClassesPath($package);
+               }
+               $this->sortPackageNamespaces();
+               $this->savePackageNamespacesAndClassesPathsToCache();
+               // The class alias map has to be rebuilt first, because ext_autoload files can contain
+               // old class names that need established class aliases.
+               $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
+               $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache($this->packages);
+               $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
+               $this->transferRuntimeClassInformationCacheEntriesToClassesCache();
+       }
+
+       /**
+        * Builds the namespace and class paths for a single package
+        *
+        * @param \TYPO3\Flow\Package\PackageInterface $package
+        * @return void
+        */
+       protected function buildPackageNamespaceAndClassesPath(\TYPO3\Flow\Package\PackageInterface $package) {
+               if ($package instanceof \TYPO3\Flow\Package\PackageInterface) {
+                       $this->buildPackageNamespace($package);
+               }
+               if ($package instanceof PackageInterface) {
+                       $this->buildPackageClassPathsForLegacyExtension($package);
+               }
+       }
+
        /**
         * Load package namespaces from cache
         *
@@ -407,44 +459,33 @@ class ClassLoader {
        }
 
        /**
-        * Build package namespaces
+        * Extracts the namespace from a package
         *
-        * @return void
-        */
-       protected function buildPackageNamespaces() {
-               /** @var $package \TYPO3\Flow\Package\Package */
-               foreach ($this->packages as $package) {
-                       $packageNamespace = $package->getNamespace();
-                       // Ignore legacy extensions with unknown vendor name
-                       if ($packageNamespace[0] !== '*') {
-                               $this->packageNamespaces[$packageNamespace] = array(
-                                       'namespaceLength' => strlen($packageNamespace),
-                                       'classesPath' => $package->getClassesPath(),
-                                       'packagePath' => $package->getPackagePath(),
-                                       'substituteNamespaceInPath' => ($package instanceof Package)
-                               );
-                       }
+        * @param \TYPO3\Flow\Package\PackageInterface $package
+        */
+       protected function buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package) {
+               $packageNamespace = $package->getNamespace();
+               // Ignore legacy extensions with unkown vendor name
+               if ($packageNamespace[0] !== '*') {
+                       $this->packageNamespaces[$packageNamespace] = array(
+                               'namespaceLength' => strlen($packageNamespace),
+                               'classesPath' => $package->getClassesPath(),
+                               'packagePath' => $package->getPackagePath(),
+                               'substituteNamespaceInPath' => ($package instanceof PackageInterface)
+                       );
                }
-               // Sort longer package namespaces first, to find specific matches before generic ones
-               $sortPackages = function($a, $b) {
-                       if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
-                               return strcmp($a, $b);
-                       }
-                       return ($lenA > $lenB) ? -1 : 1;
-               };
-               uksort($this->packageNamespaces, $sortPackages);
        }
 
        /**
-        * Builds the early autoload registry, also for usage in the class alias map
+        * Save autoload registry to cache
         *
+        * @param array $packages
         * @return void
         */
-       protected function buildClassInformationsFromFullAutoloadRegistry() {
+       protected function loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array $packages) {
                $classFileAutoloadRegistry = array();
-               foreach ($this->packages as $package) {
-                       /** @var $package Package */
-                       if ($package instanceof Package) {
+               foreach ($packages as $package) {
+                       if ($package instanceof PackageInterface) {
                                $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
                                if (is_array($classFilesFromAutoloadRegistry)) {
                                        $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
@@ -453,29 +494,36 @@ class ClassLoader {
                }
                foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
                        $lowercasedClassName = strtolower($className);
-                       if (!isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName]) && @file_exists($classFilePath)) {
-                               $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName] = array($classFilePath, $className);
+                       if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName]) && @file_exists($classFilePath)) {
+                               $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
                        }
                }
        }
 
        /**
-        * Builds the classes paths for legacy extensions with unknown vendor name
-        *
-        * @return void
+        * Transfers all entries from the early class information cache to
+        * the classes cache in order to make them persistent
         */
-       protected function buildPackageClassesPathsForLegacyExtensions() {
-               foreach ($this->packages as $package) {
-                       if ($package instanceof \TYPO3\CMS\Core\Package\PackageInterface) {
-                               $classesPath = $package->getClassesPath();
-                               $this->packageClassesPaths[$package->getPackageKey()] = $classesPath;
-                               foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
-                                       $this->packageClassesPaths[$packageToReplace] = $classesPath;
-                               }
+       protected function transferRuntimeClassInformationCacheEntriesToClassesCache() {
+               foreach ($this->runtimeClassLoadingInformationCache as $classLoadingInformation) {
+                       $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
+                       if (!$this->classesCache->has($cacheEntryIdentifier)) {
+                               $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation));
                        }
                }
        }
 
+       /**
+        * @param PackageInterface $package
+        * @return void
+        */
+       protected function buildPackageClassPathsForLegacyExtension(PackageInterface $package) {
+               $this->packageClassesPaths[$package->getPackageKey()] = $package->getClassesPath();
+               foreach (array_keys($package->getPackageReplacementKeys()) as $packageToReplace) {
+                       $this->packageClassesPaths[$packageToReplace] = $package->getClassesPath();
+               }
+       }
+
        /**
         * Save package namespaces and classes paths to cache
         *
@@ -491,17 +539,32 @@ class ClassLoader {
                }
        }
 
+       /**
+        * Sorts longer package namespaces first, to find specific matches before generic ones
+        *
+        * @return void
+        */
+       protected function sortPackageNamespaces() {
+               $sortPackages = function ($a, $b) {
+                       if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
+                               return strcmp($a, $b);
+                       }
+                       return $lenA > $lenB ? -1 : 1;
+               };
+               uksort($this->packageNamespaces, $sortPackages);
+       }
+
        /**
         * This method is necessary for the early loading of the cores autoload registry
         *
         * @param array $classFileAutoloadRegistry
         * @return void
         */
-       public function setEarlyClassInformationsFromAutoloadRegistry($classFileAutoloadRegistry) {
+       public function setRuntimeClassLoadingInformationFromAutoloadRegistry(array $classFileAutoloadRegistry) {
                foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
                        $lowercasedClassName = strtolower($className);
-                       if (!isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName])) {
-                               $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName] = array($classFilePath, $className);
+                       if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
+                               $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
                        }
                }
        }
index a42ff6e..502698e 100644 (file)
@@ -139,7 +139,8 @@ class Package extends \TYPO3\Flow\Package\Package implements PackageInterface {
         * @return array
         */
        public function getPackageReplacementKeys() {
-               return $this->getComposerManifest('replace') ?: array();
+               // The cast to array is required since the manifest returns data with type mixed
+               return (array)$this->getComposerManifest('replace') ?: array();
        }
 
        /**
index b9fadd7..23b2e14 100644 (file)
@@ -16,11 +16,16 @@ namespace TYPO3\CMS\Core\Package;
  *
  * @api
  */
-interface PackageInterface {
+interface PackageInterface extends \TYPO3\Flow\Package\PackageInterface {
 
        /**
         * @return array
         */
        public function getPackageReplacementKeys();
 
+       /**
+        * @return array
+        */
+       public function getClassFilesFromAutoloadRegistry();
+
 }
index 8ec3ea8..6486667 100644 (file)
@@ -58,6 +58,11 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
         */
        protected $packageAliasMap = array();
 
+       /**
+        * @var array
+        */
+       protected $runtimeActivatedPackages = array();
+
        /**
         * Adjacency matrix for the dependency graph (DAG)
         *
@@ -511,7 +516,7 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
                if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
                        $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
                }
-               return parent::isPackageActive($packageKey);
+               return parent::isPackageActive($packageKey) || isset($this->runtimeActivatedPackages[$packageKey]);
        }
 
        /**
@@ -530,6 +535,18 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
                parent::activatePackage($package->getPackageKey());
        }
 
+       /**
+        * Enables packages during runtime, but no class aliases will be available
+        *
+        * @param string $packageKey
+        * @api
+        */
+       public function activatePackageDuringRuntime($packageKey) {
+               $package = $this->getPackage($packageKey);
+               $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
+               $this->classLoader->addRuntimeActivatedPackage($package);
+       }
+
 
        /**
         * @param string $packageKey
@@ -572,6 +589,17 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
                parent::refreezePackage($package->getPackageKey());
        }
 
+       /**
+        * Returns an array of \TYPO3\Flow\Package objects of all active packages.
+        * A package is active, if it is available and has been activated in the package
+        * manager settings. This method returns runtime activated packages too
+        *
+        * @return array Array of \TYPO3\Flow\Package\PackageInterface
+        * @api
+        */
+       public function getActivePackages() {
+               return array_merge(parent::getActivePackages(), $this->runtimeActivatedPackages);
+       }
 
        /**
         * Get packages of specific type