Merge pull request #22552 from colemanw/searchKitConditionalCss
[civicrm-core.git] / CRM / Extension / MixinLoader.php
index 2b8d5277be08769c96ab1c07c19c9ef1782be006..edb67a0814d1a946f8051ba59fba34c1d88081a3 100644 (file)
  */
 
 /**
- * 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, '>=');
   }
 
 }