From 1795aaf579e610cec8bb775bc0c0cf119832a662 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 2 Dec 2021 22:30:50 -0800 Subject: [PATCH] (REF) Mixins - Move more aspects of scanning from MixinLoader to MixinScanner --- CRM/Extension/MixinLoader.php | 148 ++----------------------------- CRM/Extension/MixinScanner.php | 153 +++++++++++++++++++++++++++++++-- CRM/Extension/System.php | 11 +-- 3 files changed, 155 insertions(+), 157 deletions(-) diff --git a/CRM/Extension/MixinLoader.php b/CRM/Extension/MixinLoader.php index 2b8d5277be..391eddf141 100644 --- a/CRM/Extension/MixinLoader.php +++ b/CRM/Extension/MixinLoader.php @@ -14,137 +14,12 @@ */ 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; - - /** - * @var array - * Ex: ['civix' => ['1.0.0' => 'path/to/civix@1.0.0.mixin.php']] - */ - protected $allFuncFiles = []; - - /** - * @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; - } - - /** - * @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; - } - - 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 []; - } - - /** - * 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 ?? []; - - $sortByVer = function ($a, $b) { - return version_compare($a, $b /* ignore third arg */); - }; - foreach (array_keys($allFuncs) as $name) { - uksort($allFuncs[$name], $sortByVer); - } - - $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)); - } - } - } - - $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."); - } - + public function run(CRM_Extension_BootCache $bootCache, array $liveFuncFiles, array $mixInfos): void { // == WIP == // //Do mixins run strictly once (during boot)? Or could they run twice? Or incrementally? Some edge-cases: @@ -161,7 +36,7 @@ class CRM_Extension_MixinLoader { // 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,15 +48,15 @@ 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])) { continue; } - if (isset($funcsByFile[$this->liveFuncFiles[$verExpr]])) { - call_user_func($funcsByFile[$this->liveFuncFiles[$verExpr]], $ext, $bootCache); + if (isset($funcsByFile[$liveFuncFiles[$verExpr]])) { + call_user_func($funcsByFile[$liveFuncFiles[$verExpr]], $ext, $bootCache); $done[$doneId] = 1; } else { @@ -189,19 +64,6 @@ class CRM_Extension_MixinLoader { } } } - - 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, '>='); } } diff --git a/CRM/Extension/MixinScanner.php b/CRM/Extension/MixinScanner.php index 433f647d0b..8e4039b604 100644 --- a/CRM/Extension/MixinScanner.php +++ b/CRM/Extension/MixinScanner.php @@ -31,6 +31,25 @@ class CRM_Extension_MixinScanner { */ protected $relativeBases; + /** + * @var array + * Ex: ['civix' => ['1.0.0' => 'path/to/civix@1.0.0.mixin.php']] + */ + protected $allFuncFiles = []; + + /** + * @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; + + /** + * @var \CRM_Extension_MixInfo[] + */ + protected $mixInfos = []; + /** * CRM_Extension_ClassLoader constructor. * @param \CRM_Extension_Mapper|NULL $mapper @@ -57,27 +76,132 @@ class CRM_Extension_MixinScanner { } /** - * @return \CRM_Extension_MixinLoader + * @return array{0: funcFiles, 1: mixInfos} */ - public function createLoader() { - $l = new CRM_Extension_MixinLoader(); + public function build() { + $this->scan(); + return $this->compile(); + } + /** + * Search through known extensions + */ + protected function scan() { foreach ($this->getInstalledKeys() as $key) { try { $path = $this->mapper->keyToBasePath($key); - $l->addMixInfo($this->createMixInfo($path . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME)); - $l->addFunctionFiles($this->findFunctionFiles("$path/mixin/*@*.mixin.php")); - $l->addFunctionFiles($this->findFunctionFiles("$path/mixin/*@*/mixin.php"), TRUE); + $this->addMixInfo($this->createMixInfo($path . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME)); + $this->addFunctionFiles($this->findFunctionFiles("$path/mixin/*@*.mixin.php")); + $this->addFunctionFiles($this->findFunctionFiles("$path/mixin/*@*/mixin.php"), TRUE); } catch (CRM_Extension_Exception_ParseException $e) { error_log(sprintf('MixinScanner: Failed to read extension (%s)', $key)); } } - $l->addFunctionFiles($this->findFunctionFiles(Civi::paths()->getPath('[civicrm.root]/mixin/*@*.mixin.php'))); - $l->addFunctionFiles($this->findFunctionFiles(Civi::paths()->getPath('[civicrm.root]/mixin/*@*/mixin.php')), TRUE); + $this->addFunctionFiles($this->findFunctionFiles(Civi::paths()->getPath('[civicrm.root]/mixin/*@*.mixin.php'))); + $this->addFunctionFiles($this->findFunctionFiles(Civi::paths()->getPath('[civicrm.root]/mixin/*@*/mixin.php')), TRUE); + } + + /** + * 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. + */ + protected function compile() { + $this->liveFuncFiles = []; + $allFuncs = $this->allFuncFiles ?? []; + + $sortByVer = function ($a, $b) { + return version_compare($a, $b /* ignore third arg */); + }; + foreach (array_keys($allFuncs) as $name) { + uksort($allFuncs[$name], $sortByVer); + } + + $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)); + } + } + } - return $l->compile(); + $this->allFuncFiles = NULL; + + return [$this->liveFuncFiles, $this->mixInfos]; + } + + /** + * @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; + } + + /** + * @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; + } + + 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 []; } /** @@ -145,4 +269,15 @@ class CRM_Extension_MixinScanner { return $file; } + /** + * @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, '>='); + } + } diff --git a/CRM/Extension/System.php b/CRM/Extension/System.php index 34cae07f56..ed41033348 100644 --- a/CRM/Extension/System.php +++ b/CRM/Extension/System.php @@ -246,16 +246,17 @@ class CRM_Extension_System { public function applyMixins($force = FALSE) { $cache = $this->getCache(); - $cachedMixinLoader = $force ? NULL : $cache->get('mixinLoader'); + $cachedScan = $force ? NULL : $cache->get('mixinScan'); $cachedBootData = $force ? NULL : $cache->get('mixinBoot'); - $mixinLoader = $cachedMixinLoader ?: (new CRM_Extension_MixinScanner($this->mapper, $this->manager, TRUE))->createLoader(); + [$funcFiles, $mixInfos] = $cachedScan ?: (new CRM_Extension_MixinScanner($this->mapper, $this->manager, TRUE))->build(); $bootData = $cachedBootData ?: new CRM_Extension_BootCache(); - $mixinLoader->run($bootData); + $mixinLoader = new CRM_Extension_MixinLoader(); + $mixinLoader->run($bootData, $funcFiles, $mixInfos); - if ($cachedMixinLoader === NULL) { - $cache->set('mixinLoader', $mixinLoader, 24 * 60 * 60); + if ($cachedScan === NULL) { + $cache->set('mixinScan', [$funcFiles, $mixInfos], 24 * 60 * 60); } if ($cachedBootData === NULL) { $bootData->lock(); -- 2.25.1