--- /dev/null
+<?php
+namespace Civi\Afform;
+
+class Symbols {
+
+ /**
+ * @var array
+ * Array(string $element => 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]++;
+ }
+
+}
* @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',
[]
*/
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);
}
/**
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) {
'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.
}
}
+/**
+ * Scan the list of Angular modules and inject automatic-requirements.
+ *
+ * TLDR: if an afform uses element "<other-el/>", 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()