X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=CRM%2FExtension%2FMixinLoader.php;h=edb67a0814d1a946f8051ba59fba34c1d88081a3;hb=1f43c7f7412b5703c4bda33297b33b38bcc1149b;hp=2b8d5277be08769c96ab1c07c19c9ef1782be006;hpb=e1092ab13de3fe806fc35e668aa9ee9c75347800;p=civicrm-core.git diff --git a/CRM/Extension/MixinLoader.php b/CRM/Extension/MixinLoader.php index 2b8d5277be..edb67a0814 100644 --- a/CRM/Extension/MixinLoader.php +++ b/CRM/Extension/MixinLoader.php @@ -10,158 +10,49 @@ */ /** - * The MixinLoader tracks a list of extensions and mixins. + * The MixinLoader gets a list of extensions and mixins - then loads them. */ class CRM_Extension_MixinLoader { /** - * @var \CRM_Extension_MixInfo[] - */ - protected $mixInfos = []; - - /** - * @var array|null - * If we have not scanned for live funcs, then NULL. - * Otherwise, every live version-requirement is mapped to the corresponding file. - * Ex: ['civix@1' => 'path/to/civix@1.0.0.mixin.php'] - */ - protected $liveFuncFiles = NULL; - - /** + * List extension-mixins that have been loaded already. + * * @var array - * Ex: ['civix' => ['1.0.0' => 'path/to/civix@1.0.0.mixin.php']] */ - protected $allFuncFiles = []; + protected $done = []; - /** - * @param CRM_Extension_MixInfo $mix - * @return static - * @throws \CRM_Extension_Exception_ParseException - */ - public function addMixInfo(CRM_Extension_MixInfo $mix) { - $this->mixInfos[$mix->longName] = $mix; - return $this; - } + public function run($force = FALSE) { + $system = CRM_Extension_System::singleton(); + $cache = $system->getCache(); - /** - * @param array|string $files - * Ex: 'path/to/some/file@1.0.0.mixin.php' - * @param bool $deepRead - * If TRUE, then the file will be read to find metadata. - * @return $this - */ - public function addFunctionFiles($files, $deepRead = FALSE) { - $files = (array) $files; - foreach ($files as $file) { - if (preg_match(';^([^@]+)@([^@]+)\.mixin\.php$;', basename($file), $m)) { - $this->allFuncFiles[$m[1]][$m[2]] = $file; - continue; - } + $cachedScan = $force ? NULL : $cache->get('mixinScan'); + $cachedBootData = $force ? NULL : $cache->get('mixinBoot'); - if ($deepRead) { - $header = $this->loadFunctionFileHeader($file); - if (isset($header['mixinName'], $header['mixinVersion'])) { - $this->allFuncFiles[$header['mixinName']][$header['mixinVersion']] = $file; - continue; - } - else { - error_log(sprintf('MixinLoader: Invalid mixin header for "%s". @mixinName and @mixinVersion required.', $file)); - continue; - } - } - - error_log(sprintf('MixinLoader: File \"%s\" cannot be parsed.', $file)); - } - return $this; - } - - private function loadFunctionFileHeader($file) { - $php = file_get_contents($file, TRUE); - foreach (token_get_all($php) as $token) { - if (is_array($token) && in_array($token[0], [T_DOC_COMMENT, T_COMMENT, T_FUNC_C, T_METHOD_C, T_TRAIT_C, T_CLASS_C])) { - return \Civi\Api4\Utils\ReflectionUtils::parseDocBlock($token[1]); - } - } - return []; - } + [$funcFiles, $mixInfos] = $cachedScan ?: (new CRM_Extension_MixinScanner($system->getMapper(), $system->getManager(), TRUE))->build(); + $bootData = $cachedBootData ?: new CRM_Extension_BootCache(); - /** - * Optimize the metadata, removing information that is not needed at runtime. - * - * Steps: - * - * - Remove any unnecessary $mixInfos (ie they have no mixins). - * - Given the available versions and expectations, pick the best $liveFuncFiles. - * - Drop $allFuncFiles. - */ - public function compile() { - $this->liveFuncFiles = []; - $allFuncs = $this->allFuncFiles ?? []; + $this->loadMixins($bootData, $funcFiles, $mixInfos); - $sortByVer = function ($a, $b) { - return version_compare($a, $b /* ignore third arg */); - }; - foreach (array_keys($allFuncs) as $name) { - uksort($allFuncs[$name], $sortByVer); + if ($cachedScan === NULL) { + $cache->set('mixinScan', [$funcFiles, $mixInfos], 24 * 60 * 60); } - - $this->mixInfos = array_filter($this->mixInfos, function(CRM_Extension_MixInfo $mixInfo) { - return !empty($mixInfo->mixins); - }); - - foreach ($this->mixInfos as $ext) { - /** @var \CRM_Extension_MixInfo $ext */ - foreach ($ext->mixins as $verExpr) { - list ($name, $expectVer) = explode('@', $verExpr); - $matchFile = NULL; - // NOTE: allFuncs[$name] is sorted by increasing version number. Choose highest satisfactory match. - foreach ($allFuncs[$name] ?? [] as $availVer => $availFile) { - if (static::satisfies($expectVer, $availVer)) { - $matchFile = $availFile; - } - } - if ($matchFile) { - $this->liveFuncFiles[$verExpr] = $matchFile; - } - else { - error_log(sprintf('MixinLoader: Failed to locate match for "%s"', $verExpr)); - } - } + if ($cachedBootData === NULL) { + $bootData->lock(); + $cache->set('mixinBoot', $bootData, 24 * 60 * 60); } - - $this->allFuncFiles = NULL; - - return $this; } /** * Load all extensions and call their respective function-files. * - * @return static * @throws \CRM_Core_Exception */ - public function run(CRM_Extension_BootCache $bootCache) { - if ($this->liveFuncFiles === NULL) { - throw new CRM_Core_Exception("Premature initialization. MixinLoader has not identified live functions."); - } - - // == WIP == - // - //Do mixins run strictly once (during boot)? Or could they run twice? Or incrementally? Some edge-cases: - // - Mixins should make changes via dispatcher() and container(). If there's a Civi::reset(), then these things go away. We'll need to - // re-register. (Example scenario: unit-testing) - // - Mixins register for every active module. If a new module is enabled, then we haven't had a chance to run on the new extension. - // - Mixins register for every active module. If an old module is disabled, then there may be old listeners/services lingering. - if (!isset(\Civi::$statics[__CLASS__]['done'])) { - \Civi::$statics[__CLASS__]['done'] = []; - } - $done = &\Civi::$statics[__CLASS__]['done']; - + protected function loadMixins(CRM_Extension_BootCache $bootCache, array $liveFuncFiles, array $mixInfos): void { // Read each live func-file once, even if there's some kind of Civi::reset(). This avoids hard-crash where the func-file registers a PHP class/function/interface. // Granted, PHP symbols require care to avoid conflicts between `mymixin@1.0` and `mymixin@2.0` -- but you can deal with that. For minor-versions, you're // safe because we deduplicate. static $funcsByFile = []; - foreach ($this->liveFuncFiles as $verExpr => $file) { + foreach ($liveFuncFiles as $verExpr => $file) { if (!isset($funcsByFile[$file])) { $func = include_once $file; if (is_callable($func)) { @@ -173,35 +64,22 @@ class CRM_Extension_MixinLoader { } } - foreach ($this->mixInfos as $ext) { + foreach ($mixInfos as $ext) { /** @var \CRM_Extension_MixInfo $ext */ foreach ($ext->mixins as $verExpr) { $doneId = $ext->longName . '::' . $verExpr; - if (isset($done[$doneId])) { + if (isset($this->done[$doneId])) { continue; } - if (isset($funcsByFile[$this->liveFuncFiles[$verExpr]])) { - call_user_func($funcsByFile[$this->liveFuncFiles[$verExpr]], $ext, $bootCache); - $done[$doneId] = 1; + if (isset($funcsByFile[$liveFuncFiles[$verExpr]])) { + call_user_func($funcsByFile[$liveFuncFiles[$verExpr]], $ext, $bootCache); + $this->done[$doneId] = 1; } else { error_log(sprintf('MixinLoader: Failed to load "%s" for extension "%s"', $verExpr, $ext->longName)); } } } - - return $this; - } - - /** - * @param string $expectVer - * @param string $actualVer - * @return bool - */ - private static function satisfies($expectVer, $actualVer) { - [$expectMajor] = explode('.', $expectVer); - [$actualMajor] = explode('.', $actualVer); - return ($expectMajor == $actualMajor) && version_compare($actualVer, $expectVer, '>='); } }