From 77dccccbeb37630ba0b4a98c02c3d22b10a2d6d2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 18 Oct 2019 15:05:22 -0700 Subject: [PATCH] Automatically detect requirements in *.aff.html PART A: EXPORT LIST Suppose you create a form `downstreamForm.aff.html`. The metadata for `downstreamForm` will indicate that it exports an element ``: ```php $angularModules['downstreamForm']['exports']['el'] = ['downstream-form']; ``` PART B: AUTO-REQUIRES Suppose that: 1. he form `downstreamForm.aff.html` uses element ``. 2. The `$angularModules['upsteamMod']['exports']['el']` defines an exported element `upstream-el`. Then the `downstreamForm` module will automatically requires the `upstreamMod`, as in ```php $angularModules['downstreamForm']['requires'][] = 'upstreamMod'; ``` --- ext/afform/core/CRM/Afform/AfformScanner.php | 2 +- ext/afform/core/Civi/Afform/Symbols.php | 68 +++++++++++ ext/afform/core/afform.php | 116 +++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 ext/afform/core/Civi/Afform/Symbols.php diff --git a/ext/afform/core/CRM/Afform/AfformScanner.php b/ext/afform/core/CRM/Afform/AfformScanner.php index 589602369d..fe2c91f6cc 100644 --- a/ext/afform/core/CRM/Afform/AfformScanner.php +++ b/ext/afform/core/CRM/Afform/AfformScanner.php @@ -134,7 +134,7 @@ class CRM_Afform_AfformScanner { $defaults = [ 'name' => $name, - 'requires' => explode(',', self::DEFAULT_REQUIRES), + 'requires' => [], 'title' => '', 'description' => '', 'is_public' => FALSE, diff --git a/ext/afform/core/Civi/Afform/Symbols.php b/ext/afform/core/Civi/Afform/Symbols.php new file mode 100644 index 0000000000..b933afb11f --- /dev/null +++ b/ext/afform/core/Civi/Afform/Symbols.php @@ -0,0 +1,68 @@ + int $count). + */ + public $elements = []; + + /** + * @var array + * Array(string $class => int $count). + */ + public $classes = []; + + /** + * @var array + * Array(string $attr => int $count). + */ + public $attributes = []; + + /** + * @param string $html + * @return static + */ + public static function scan($html) { + $symbols = new static(); + $doc = new \DOMDocumentWrapper($html, 'text/html'); + $symbols->scanNode($doc->root); + return $symbols; + } + + protected function scanNode(\DOMNode $node) { + if ($node instanceof \DOMElement) { + + self::increment($this->elements, $node->tagName); + + foreach ($node->childNodes as $childNode) { + $this->scanNode($childNode); + } + + foreach ($node->attributes as $attribute) { + $this->scanNode($attribute); + } + } + + elseif ($node instanceof \DOMAttr) { + self::increment($this->attributes, $node->nodeName); + + if ($node->nodeName === 'class') { + $classes = explode(' ', $node->nodeValue); + foreach ($classes as $class) { + self::increment($this->classes, $class); + } + } + } + } + + private static function increment(&$arr, $key) { + if (!isset($arr[$key])) { + $arr[$key] = 0; + } + $arr[$key]++; + } + +} diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index 28caaf8952..605a186f18 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -37,6 +37,7 @@ function _afform_fields_filter($params) { * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container */ function afform_civicrm_container($container) { + $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__)); $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition( 'CRM_Afform_AfformScanner', [] @@ -50,8 +51,15 @@ function afform_civicrm_container($container) { */ function afform_civicrm_config(&$config) { _afform_civix_civicrm_config($config); + + if (isset(Civi::$statics[__FUNCTION__])) { + return; + } + Civi::$statics[__FUNCTION__] = 1; + // Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processContacts'], -500); Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processGenericEntity'], -1000); + Civi::dispatcher()->addListener('hook_civicrm_angularModules', '_afform_civicrm_angularModules_autoReq', -1000); } /** @@ -155,6 +163,7 @@ function afform_civicrm_caseTypes(&$caseTypes) { function afform_civicrm_angularModules(&$angularModules) { _afform_civix_civicrm_angularModules($angularModules); + /** @var CRM_Afform_AfformScanner $scanner */ $scanner = Civi::service('afform_scanner'); $names = array_keys($scanner->findFilePaths()); foreach ($names as $name) { @@ -164,6 +173,11 @@ function afform_civicrm_angularModules(&$angularModules) { 'js' => ['assetBuilder://afform.js?name=' . urlencode($name)], 'requires' => $meta['requires'], 'basePages' => [], + 'exports' => [ + // Each afform is an attribute and an element. + 'el' => [_afform_angular_module_name($name, 'dash')], + 'attr' => [_afform_angular_module_name($name, 'dash')], + ], ]; // FIXME: The HTML layout template is embedded in the JS asset. @@ -173,6 +187,108 @@ function afform_civicrm_angularModules(&$angularModules) { } } +/** + * Scan the list of Angular modules and inject automatic-requirements. + * + * TLDR: if an afform uses element "", and if another module defines + * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then + * the 'otherMod' is automatically required. + * + * @param \Civi\Core\Event\GenericHookEvent $e + * @see CRM_Utils_Hook::angularModules() + */ +function _afform_civicrm_angularModules_autoReq($e) { + /** @var CRM_Afform_AfformScanner $scanner */ + $scanner = Civi::service('afform_scanner'); + $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules))); + $depCache = CRM_Utils_Cache::create([ + 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6), + 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'], + 'withArray' => 'fast', + 'prefetch' => TRUE, + ]); + $depCacheTtl = 2 * 60 * 60; + + $revMap = _afform_reverse_deps($e->angularModules); + + $formNames = array_keys($scanner->findFilePaths()); + foreach ($formNames as $formName) { + $angModule = _afform_angular_module_name($formName, 'camel'); + $cacheLine = $depCache->get($formName, NULL); + + $jFile = $scanner->findFilePath($formName, 'aff.json'); + $hFile = $scanner->findFilePath($formName, 'aff.html'); + + $jStat = stat($jFile); + $hStat = stat($hFile); + + if ($cacheLine === NULL) { + $needsUpdate = TRUE; + } + elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) { + $needsUpdate = TRUE; + } + elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) { + $needsUpdate = TRUE; + } + elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) { + $needsUpdate = TRUE; + } + elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) { + $needsUpdate = TRUE; + } + else { + $needsUpdate = FALSE; + } + + if ($needsUpdate) { + $cacheLine = [ + 'js' => $jStat['size'] ?? NULL, + 'jm' => $jStat['mtime'] ?? NULL, + 'hs' => $hStat['size'] ?? NULL, + 'hm' => $hStat['mtime'] ?? NULL, + 'r' => array_values(array_unique(array_merge( + [CRM_Afform_AfformScanner::DEFAULT_REQUIRES], + $e->angularModules[$angModule]['requires'] ?? [], + _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap) + ))), + ]; + // print_r(['cache update:' . $formName => $cacheLine]); + $depCache->set($formName, $cacheLine, $depCacheTtl); + } + + $e->angularModules[$angModule]['requires'] = $cacheLine['r']; + } +} + +/** + * @param $angularModules + * @return array + * 'attr': array(string $attrName => string $angModuleName) + * 'el': array(string $elementName => string $angModuleName) + */ +function _afform_reverse_deps($angularModules) { + $revMap = []; + foreach (['attr', 'el'] as $exportType) { + $revMap[$exportType] = []; + foreach (array_keys($angularModules) as $module) { + if (isset($angularModules[$module]['exports'][$exportType])) { + foreach ($angularModules[$module]['exports'][$exportType] as $exportItem) { + $revMap[$exportType][$exportItem] = $module; + } + } + } + } + return $revMap; +} + +function _afform_reverse_deps_find($formName, $html, $revMap) { + $symbols = \Civi\Afform\Symbols::scan($html); + $elems = array_intersect_key($revMap['el'], $symbols->elements); + $attrs = array_intersect_key($revMap['attr'], $symbols->attributes); + return array_values(array_unique(array_merge($elems, $attrs))); +} + /** * @param \Civi\Angular\Manager $angular * @see CRM_Utils_Hook::alterAngular() -- 2.25.1