| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 6 | | This work is published under the GNU AGPLv3 license with some | |
| 7 | | permitted exceptions and without any warranty. For full license | |
| 8 | | and copyright information, see https://civicrm.org/licensing | |
| 9 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * The MixinLoader gets a list of extensions and mixins - then loads them. |
| 14 | */ |
| 15 | class CRM_Extension_MixinLoader { |
| 16 | |
| 17 | public function run($force = FALSE) { |
| 18 | $system = CRM_Extension_System::singleton(); |
| 19 | $cache = $system->getCache(); |
| 20 | |
| 21 | $cachedScan = $force ? NULL : $cache->get('mixinScan'); |
| 22 | $cachedBootData = $force ? NULL : $cache->get('mixinBoot'); |
| 23 | |
| 24 | [$funcFiles, $mixInfos] = $cachedScan ?: (new CRM_Extension_MixinScanner($system->getMapper(), $system->getManager(), TRUE))->build(); |
| 25 | $bootData = $cachedBootData ?: new CRM_Extension_BootCache(); |
| 26 | |
| 27 | $this->loadMixins($bootData, $funcFiles, $mixInfos); |
| 28 | |
| 29 | if ($cachedScan === NULL) { |
| 30 | $cache->set('mixinScan', [$funcFiles, $mixInfos], 24 * 60 * 60); |
| 31 | } |
| 32 | if ($cachedBootData === NULL) { |
| 33 | $bootData->lock(); |
| 34 | $cache->set('mixinBoot', $bootData, 24 * 60 * 60); |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * Load all extensions and call their respective function-files. |
| 40 | * |
| 41 | * @throws \CRM_Core_Exception |
| 42 | */ |
| 43 | protected function loadMixins(CRM_Extension_BootCache $bootCache, array $liveFuncFiles, array $mixInfos): void { |
| 44 | // == WIP == |
| 45 | // |
| 46 | //Do mixins run strictly once (during boot)? Or could they run twice? Or incrementally? Some edge-cases: |
| 47 | // - Mixins should make changes via dispatcher() and container(). If there's a Civi::reset(), then these things go away. We'll need to |
| 48 | // re-register. (Example scenario: unit-testing) |
| 49 | // - 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. |
| 50 | // - Mixins register for every active module. If an old module is disabled, then there may be old listeners/services lingering. |
| 51 | if (!isset(\Civi::$statics[__CLASS__]['done'])) { |
| 52 | \Civi::$statics[__CLASS__]['done'] = []; |
| 53 | } |
| 54 | $done = &\Civi::$statics[__CLASS__]['done']; |
| 55 | |
| 56 | // 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. |
| 57 | // 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 |
| 58 | // safe because we deduplicate. |
| 59 | static $funcsByFile = []; |
| 60 | foreach ($liveFuncFiles as $verExpr => $file) { |
| 61 | if (!isset($funcsByFile[$file])) { |
| 62 | $func = include_once $file; |
| 63 | if (is_callable($func)) { |
| 64 | $funcsByFile[$file] = $func; |
| 65 | } |
| 66 | else { |
| 67 | error_log(sprintf('MixinLoader: Received invalid callback from \"%s\"', $file)); |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | foreach ($mixInfos as $ext) { |
| 73 | /** @var \CRM_Extension_MixInfo $ext */ |
| 74 | foreach ($ext->mixins as $verExpr) { |
| 75 | $doneId = $ext->longName . '::' . $verExpr; |
| 76 | if (isset($done[$doneId])) { |
| 77 | continue; |
| 78 | } |
| 79 | if (isset($funcsByFile[$liveFuncFiles[$verExpr]])) { |
| 80 | call_user_func($funcsByFile[$liveFuncFiles[$verExpr]], $ext, $bootCache); |
| 81 | $done[$doneId] = 1; |
| 82 | } |
| 83 | else { |
| 84 | error_log(sprintf('MixinLoader: Failed to load "%s" for extension "%s"', $verExpr, $ext->longName)); |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | } |