CollectionTrait - Use "splats". Split out "adders". Define interfaces.
authorTim Otten <totten@civicrm.org>
Tue, 18 Aug 2020 03:45:29 +0000 (20:45 -0700)
committerSeamus Lee <seamuslee001@gmail.com>
Thu, 3 Sep 2020 22:02:17 +0000 (08:02 +1000)
In the prior commits, the signatures for `addScriptFile()`,
`addScriptUrl()`, etc are not strictly interoperable between
`CRM_Core_Resources` and `CollectionTrait`.  This is because they use
key-value options instead of positional options.  This makes it easier
disregard positional options that don't make sense (e.g.  when calling
`CRM_Core_Region::addScriptFile()`, it's silly to reserve a positional
argument for the `$region` option).

The signatures *can* be unified by using "splats" (ie `...$options`) to
accept either key-value options or backward-compatible positional options.
The ultimate is hope is that:

* `CRM_Core_Resources`, `CRM_Core_Region`, and `CRM_Core_Bundle` all
  implement the `CollectionAdderInterface`.
* `CRM_Core_Resources`, `CRM_Core_Region`, and `CRM_Core_Bundle` all
  accept options in either format (key-value or positional).
* The positional format will fade-away.

The methods in CollectionTrait are newer terrain, so it's safer to change
those signatures, so we do that first.

Note that CollectionTrait formally builds on CollectionAdderTrait. This
ensures that IDE navigation to (eg) `CRM_Core_Region::add()` and
`CRM_Core_Resources_Bundle::add()` works as expected.

CRM/Core/Region.php
CRM/Core/Resources/CollectionAdderInterface.php [new file with mode: 0644]
CRM/Core/Resources/CollectionAdderTrait.php [new file with mode: 0644]
CRM/Core/Resources/CollectionInterface.php [new file with mode: 0644]
CRM/Core/Resources/CollectionTrait.php

index 7a31455005ff1734f4b5c0973ea891ccc47af6c7..c7eab5c58418638048df690882e7081b0c15fc2e 100644 (file)
@@ -3,7 +3,7 @@
 /**
  * Maintain a set of markup/templates to inject inside various regions
  */
-class CRM_Core_Region {
+class CRM_Core_Region implements CRM_Core_Resources_CollectionInterface, CRM_Core_Resources_CollectionAdderInterface {
 
   /**
    * Obtain the content for a given region.
@@ -20,9 +20,7 @@ class CRM_Core_Region {
     return Civi::$statics[__CLASS__][$name];
   }
 
-  use CRM_Core_Resources_CollectionTrait {
-    CRM_Core_Resources_CollectionTrait::add as _add;
-  }
+  use CRM_Core_Resources_CollectionTrait;
 
   /**
    * Symbolic name of this region
diff --git a/CRM/Core/Resources/CollectionAdderInterface.php b/CRM/Core/Resources/CollectionAdderInterface.php
new file mode 100644 (file)
index 0000000..5f435ce
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * The collection-adder interface provides write-only support for a collection.
+ */
+interface CRM_Core_Resources_CollectionAdderInterface {
+
+  /**
+   * Add an item to the collection.
+   *
+   * @param array $snippet
+   * @return array
+   *   The full/computed snippet (with defaults applied).
+   * @see CRM_Core_Resources_CollectionInterface::add()
+   */
+  public function add($snippet);
+
+  // TODO public function addBundle($bundle);
+
+  /**
+   * Export permission data to the client to enable smarter GUIs.
+   *
+   * Note: Application security stems from the server's enforcement
+   * of the security logic (e.g. in the API permissions). There's no way
+   * the client can use this info to make the app more secure; however,
+   * it can produce a better-tuned (non-broken) UI.
+   *
+   * @param string|iterable $permNames
+   *   List of permission names to check/export.
+   * @return static
+   */
+  public function addPermissions($permNames);
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * @param string $code
+   *   JavaScript source code.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addScript(string $code, ...$options);
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * Options may be use key-value format (preferred) or positional format (legacy).
+   *
+   * - addScriptFile('myext', 'my.js', ['weight' => 123, 'region' => 'page-footer'])
+   * - addScriptFile('myext', 'my.js', 123, 'page-footer')
+   *
+   * @param string $ext
+   *   extension name; use 'civicrm' for core.
+   * @param string $file
+   *   file path -- relative to the extension base dir.
+   * @param array $options
+   *   Open-ended list of options (per add()).
+   *   Ex: ['weight' => 123]
+   *   Accepts some additional options:
+   *     - bool|string $translate: Whether to load translated strings for this file. Use one of:
+   *     - FALSE: Do not load translated strings.
+   *     - TRUE: Load translated strings. Use the $ext's default domain.
+   *     - string: Load translated strings. Use a specific domain.
+   *
+   * @return static
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function addScriptFile(string $ext, string $file, ...$options);
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * Options may be use key-value format (preferred) or positional format (legacy).
+   *
+   * - addScriptUrl('http://example.com/foo.js', ['weight' => 123, 'region' => 'page-footer'])
+   * - addScriptUrl('http://example.com/foo.js', 123, 'page-footer')
+   *
+   * @param string $url
+   * @param array $options
+   *   Open-ended list of options (per add())
+   * @return static
+   */
+  public function addScriptUrl(string $url, ...$options);
+
+  /**
+   * Add translated string to the js CRM object.
+   * It can then be retrived from the client-side ts() function
+   * Variable substitutions can happen from client-side
+   *
+   * Note: this function rarely needs to be called directly and is mostly for internal use.
+   * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
+   *
+   * Simple example:
+   * // From php:
+   * CRM_Core_Resources::singleton()->addString('Hello');
+   * // The string is now available to javascript code i.e.
+   * ts('Hello');
+   *
+   * Example with client-side substitutions:
+   * // From php:
+   * CRM_Core_Resources::singleton()->addString('Your %1 has been %2');
+   * // ts() in javascript works the same as in php, for example:
+   * ts('Your %1 has been %2', {1: objectName, 2: actionTaken});
+   *
+   * NOTE: This function does not work with server-side substitutions
+   * (as this might result in collisions and unwanted variable injections)
+   * Instead, use code like:
+   * CRM_Core_Resources::singleton()->addSetting(array('myNamespace' => array('myString' => ts('Your %1 has been %2', array(subs)))));
+   * And from javascript access it at CRM.myNamespace.myString
+   *
+   * @param string|array $text
+   * @param string|null $domain
+   * @return static
+   */
+  public function addString($text, $domain = 'civicrm');
+
+  /**
+   * Add a CSS content to the current page using <STYLE>.
+   *
+   * @param string $code
+   *   CSS source code.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyle(string $code, ...$options);
+
+  /**
+   * Add a CSS file to the current page using <LINK HREF>.
+   *
+   * @param string $ext
+   *   extension name; use 'civicrm' for core.
+   * @param string $file
+   *   file path -- relative to the extension base dir.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyleFile(string $ext, string $file, ...$options);
+
+  /**
+   * Add a CSS file to the current page using <LINK HREF>.
+   *
+   * @param string $url
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyleUrl(string $url, ...$options);
+
+  /**
+   * Add JavaScript variables to CRM.vars
+   *
+   * Example:
+   * From the server:
+   * CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
+   * Access var from javascript:
+   * CRM.vars.myNamespace.foo // "bar"
+   *
+   * @see https://docs.civicrm.org/dev/en/latest/standards/javascript/
+   *
+   * @param string $nameSpace
+   *   Usually the name of your extension.
+   * @param array $vars
+   *   Data to export.
+   * @param array $options
+   *   Extra processing instructions on where/how to place the data.
+   * @return static
+   */
+  public function addVars(string $nameSpace, array $vars, ...$options);
+
+  /**
+   * Add JavaScript variables to the root of the CRM object.
+   * This function is usually reserved for low-level system use.
+   * Extensions and components should generally use addVars instead.
+   *
+   * @param array $settings
+   *   Data to export.
+   * @param array $options
+   *   Extra processing instructions on where/how to place the data.
+   * @return static
+   */
+  public function addSetting(array $settings, ...$options);
+
+  /**
+   * Add JavaScript variables to the global CRM object via a callback function.
+   *
+   * @param callable $callable
+   * @return static
+   */
+  public function addSettingsFactory($callable);
+
+}
diff --git a/CRM/Core/Resources/CollectionAdderTrait.php b/CRM/Core/Resources/CollectionAdderTrait.php
new file mode 100644 (file)
index 0000000..87ab8e8
--- /dev/null
@@ -0,0 +1,382 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Class CRM_Core_Resources_CollectionTrait
+ *
+ * This is a building-block for creating classes which maintain a list of resources.
+ *
+ * The class is generally organized in two sections: First, we have core
+ * bit that manages a list of '$snippets'. Second, we have a set of helper
+ * functions which add some syntactic sugar for the snippets.
+ */
+trait CRM_Core_Resources_CollectionAdderTrait {
+
+  /**
+   * Add an item to the collection.
+   *
+   * @param array $snippet
+   * @return array
+   *   The full/computed snippet (with defaults applied).
+   * @see CRM_Core_Resources_CollectionInterface::add()
+   */
+  abstract public function add($snippet);
+
+  /**
+   * Locate the 'settings' snippet.
+   *
+   * @param array $options
+   * @return array
+   */
+  abstract protected function &findCreateSettingSnippet($options = []): array;
+
+  /**
+   * Export permission data to the client to enable smarter GUIs.
+   *
+   * Note: Application security stems from the server's enforcement
+   * of the security logic (e.g. in the API permissions). There's no way
+   * the client can use this info to make the app more secure; however,
+   * it can produce a better-tuned (non-broken) UI.
+   *
+   * @param string|iterable $permNames
+   *   List of permission names to check/export.
+   * @return static
+   */
+  public function addPermissions($permNames) {
+    // TODO: Maybe this should be its own resource type to allow smarter management?
+    $permNames = is_scalar($permNames) ? [$permNames] : $permNames;
+
+    $perms = [];
+    foreach ($permNames as $permName) {
+      $perms[$permName] = CRM_Core_Permission::check($permName);
+    }
+    return $this->addSetting([
+      'permissions' => $perms,
+    ]);
+  }
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * @param string $code
+   *   JavaScript source code.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addScript(string $code, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'script' => $code,
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * Options may be use key-value format (preferred) or positional format (legacy).
+   *
+   * - addScriptFile('myext', 'my.js', ['weight' => 123, 'region' => 'page-footer'])
+   * - addScriptFile('myext', 'my.js', 123, 'page-footer')
+   *
+   * @param string $ext
+   *   extension name; use 'civicrm' for core.
+   * @param string $file
+   *   file path -- relative to the extension base dir.
+   * @param array $options
+   *   Open-ended list of options (per add()).
+   *   Ex: ['weight' => 123]
+   *   Accepts some additional options:
+   *   - bool|string $translate: Whether to load translated strings for this file. Use one of:
+   *     - FALSE: Do not load translated strings.
+   *     - TRUE: Load translated strings. Use the $ext's default domain.
+   *     - string: Load translated strings. Use a specific domain.
+   *
+   * @return static
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function addScriptFile(string $ext, string $file, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'scriptFile' => [$ext, $file],
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add a JavaScript file to the current page using <SCRIPT SRC>.
+   *
+   * Options may be use key-value format (preferred) or positional format (legacy).
+   *
+   * - addScriptUrl('http://example.com/foo.js', ['weight' => 123, 'region' => 'page-footer'])
+   * - addScriptUrl('http://example.com/foo.js', 123, 'page-footer')
+   *
+   * @param string $url
+   * @param array $options
+   *   Open-ended list of options (per add())
+   * @return static
+   */
+  public function addScriptUrl(string $url, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'scriptUrl' => $url,
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add translated string to the js CRM object.
+   * It can then be retrived from the client-side ts() function
+   * Variable substitutions can happen from client-side
+   *
+   * Note: this function rarely needs to be called directly and is mostly for internal use.
+   * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
+   *
+   * Simple example:
+   * // From php:
+   * CRM_Core_Resources::singleton()->addString('Hello');
+   * // The string is now available to javascript code i.e.
+   * ts('Hello');
+   *
+   * Example with client-side substitutions:
+   * // From php:
+   * CRM_Core_Resources::singleton()->addString('Your %1 has been %2');
+   * // ts() in javascript works the same as in php, for example:
+   * ts('Your %1 has been %2', {1: objectName, 2: actionTaken});
+   *
+   * NOTE: This function does not work with server-side substitutions
+   * (as this might result in collisions and unwanted variable injections)
+   * Instead, use code like:
+   * CRM_Core_Resources::singleton()->addSetting(array('myNamespace' => array('myString' => ts('Your %1 has been %2', array(subs)))));
+   * And from javascript access it at CRM.myNamespace.myString
+   *
+   * @param string|array $text
+   * @param string|null $domain
+   * @return static
+   */
+  public function addString($text, $domain = 'civicrm') {
+    // TODO: Maybe this should be its own resource type to allow smarter management?
+
+    foreach ((array) $text as $str) {
+      $translated = ts($str, [
+        'domain' => ($domain == 'civicrm') ? NULL : [$domain, NULL],
+        'raw' => TRUE,
+      ]);
+
+      // We only need to push this string to client if the translation
+      // is actually different from the original
+      if ($translated != $str) {
+        $bucket = $domain == 'civicrm' ? 'strings' : 'strings::' . $domain;
+        $this->addSetting([
+          $bucket => [$str => $translated],
+        ]);
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Add a CSS content to the current page using <STYLE>.
+   *
+   * @param string $code
+   *   CSS source code.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyle(string $code, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'style' => $code,
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add a CSS file to the current page using <LINK HREF>.
+   *
+   * @param string $ext
+   *   extension name; use 'civicrm' for core.
+   * @param string $file
+   *   file path -- relative to the extension base dir.
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyleFile(string $ext, string $file, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'styleFile' => [$ext, $file],
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add a CSS file to the current page using <LINK HREF>.
+   *
+   * @param string $url
+   * @param array $options
+   *   Open-ended list of options (per add())
+   *   Ex: ['weight' => 123]
+   * @return static
+   */
+  public function addStyleUrl(string $url, ...$options) {
+    $this->add(self::mergeStandardOptions($options, [
+      'styleUrl' => $url,
+    ]));
+    return $this;
+  }
+
+  /**
+   * Add JavaScript variables to the root of the CRM object.
+   * This function is usually reserved for low-level system use.
+   * Extensions and components should generally use addVars instead.
+   *
+   * @param array $settings
+   *   Data to export.
+   * @param array $options
+   *   Extra processing instructions on where/how to place the data.
+   * @return static
+   */
+  public function addSetting(array $settings, ...$options) {
+    $s = &$this->findCreateSettingSnippet($options);
+    $s['settings'] = self::mergeSettings($s['settings'], $settings);
+    return $this;
+  }
+
+  /**
+   * Add JavaScript variables to the global CRM object via a callback function.
+   *
+   * @param callable $callable
+   * @return static
+   */
+  public function addSettingsFactory($callable) {
+    $s = &$this->findCreateSettingSnippet();
+    $s['settingsFactories'][] = $callable;
+    return $this;
+  }
+
+  /**
+   * Add JavaScript variables to CRM.vars
+   *
+   * Example:
+   *   From the server:
+   *     CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
+   *   Access var from javascript:
+   *     CRM.vars.myNamespace.foo // "bar"
+   *
+   * @see https://docs.civicrm.org/dev/en/latest/standards/javascript/
+   *
+   * @param string $nameSpace
+   *   Usually the name of your extension.
+   * @param array $vars
+   * @param array $options
+   *   There are no supported options.
+   * @return static
+   */
+  public function addVars(string $nameSpace, array $vars, ...$options) {
+    $s = &$this->findCreateSettingSnippet($options);
+    $s['settings']['vars'][$nameSpace] = self::mergeSettings(
+      $s['settings']['vars'][$nameSpace] ?? [],
+      $vars
+    );
+    return $this;
+  }
+
+  /**
+   * Given the "$options" for "addScriptUrl()" (etal), normalize the contents
+   * and potentially add more.
+   *
+   * @param array $splats
+   *   A list of options, as represented by the splat mechanism ("...$options").
+   *   This may appear in one of two ways:
+   *   - New (String Index): as in `addFoo($foo, array $options)`
+   *   - Old (Numeric Index): as in `addFoo($foo, int $weight = X, string $region = Y, bool $translate = X)`
+   * @param array $defaults
+   *   List of values to merge into $options.
+   * @return array
+   */
+  public static function mergeStandardOptions(array $splats, array $defaults = []) {
+    $count = count($splats);
+    switch ($count) {
+      case 0:
+        // Common+simple case: No splat options. We can short-circuit.
+        return $defaults;
+
+      case 1:
+        // Might be new format (key-value pairs) or old format
+        $parsed = is_array($splats[0]) ? $splats[0] : ['weight' => $splats[0]];
+        break;
+
+      case 2:
+        $parsed = ['weight' => $splats[0], 'region' => $splats[1]];
+        break;
+
+      case 3:
+        $parsed = ['weight' => $splats[0], 'region' => $splats[1], 'translate' => $splats[2]];
+        break;
+
+      default:
+        throw new \RuntimeException("Cannot resolve resource options. For clearest behavior, pass options in key-value format.");
+    }
+
+    return array_merge($defaults, $parsed);
+  }
+
+  /**
+   * Given the "$options" for "addSetting()" (etal), normalize the contents
+   * and potentially add more.
+   *
+   * @param array $splats
+   *   A list of options, as represented by the splat mechanism ("...$options").
+   *   This may appear in one of two ways:
+   *   - New (String Index): as in `addFoo($foo, array $options)`
+   *   - Old (Numeric Index): as in `addFoo($foo, int $weight = X, string $region = Y, bool $translate = X)`
+   * @param array $defaults
+   *   List of values to merge into $options.
+   * @return array
+   */
+  public static function mergeSettingOptions(array $splats, array $defaults = []) {
+    $count = count($splats);
+    switch ($count) {
+      case 0:
+        // Common+simple case: No splat options. We can short-circuit.
+        return $defaults;
+
+      case 1:
+        // Might be new format (key-value pairs) or old format
+        $parsed = is_array($splats[0]) ? $splats[0] : ['region' => $splats[0]];
+        break;
+
+      default:
+        throw new \RuntimeException("Cannot resolve resource options. For clearest behavior, pass options in key-value format.");
+    }
+
+    return array_merge($defaults, $parsed);
+  }
+
+  /**
+   * @param array $settings
+   * @param array $additions
+   * @return array
+   *   combination of $settings and $additions
+   */
+  public static function mergeSettings(array $settings, array $additions): array {
+    foreach ($additions as $k => $v) {
+      if (isset($settings[$k]) && is_array($settings[$k]) && is_array($v)) {
+        $v += $settings[$k];
+      }
+      $settings[$k] = $v;
+    }
+    return $settings;
+  }
+
+}
diff --git a/CRM/Core/Resources/CollectionInterface.php b/CRM/Core/Resources/CollectionInterface.php
new file mode 100644 (file)
index 0000000..a00c881
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Class CRM_Core_Resources_CollectionInterface
+ *
+ * This is a building-block for creating classes which maintain a list of resources.
+ */
+interface CRM_Core_Resources_CollectionInterface {
+
+  /**
+   * Add an item to the collection. For example, when working with 'page-header' collection:
+   *
+   * ```
+   * CRM_Core_Region::instance('page-header')->add(array(
+   *   'markup' => '<div style="color:red">Hello!</div>',
+   * ));
+   * CRM_Core_Region::instance('page-header')->add(array(
+   *   'script' => 'alert("Hello");',
+   * ));
+   * CRM_Core_Region::instance('page-header')->add(array(
+   *   'template' => 'CRM/Myextension/Extra.tpl',
+   * ));
+   * CRM_Core_Region::instance('page-header')->add(array(
+   *   'callback' => 'myextension_callback_function',
+   * ));
+   * ```
+   *
+   * Note: This function does not perform any extra encoding of markup, script code, or etc. If
+   * you're passing in user-data, you must clean it yourself.
+   *
+   * @param array $snippet
+   *   Array; keys:.
+   *   - type: string (auto-detected for markup, template, callback, script, scriptFile, scriptUrl, jquery, style, styleFile, styleUrl)
+   *   - name: string, optional
+   *   - weight: int, optional; default=1
+   *   - disabled: int, optional; default=0
+   *   - markup: string, HTML; required (for type==markup)
+   *   - template: string, path; required (for type==template)
+   *   - callback: mixed; required (for type==callback)
+   *   - arguments: array, optional (for type==callback)
+   *   - script: string, Javascript code
+   *   - scriptFile: array, the name of the extension and file. Ex: ['civicrm', 'js/foo.js']
+   *   - scriptUrl: string, URL of a Javascript file
+   *   - jquery: string, Javascript code which runs inside a jQuery(function($){...}); block
+   *   - settings: array, list of static values to convey.
+   *   - style: string, CSS code
+   *   - styleFile: array, the name of the extension and file. Ex: ['civicrm', 'js/foo.js']
+   *   - styleUrl: string, URL of a CSS file
+   *
+   * @return array
+   *   The full/computed snippet (with defaults applied).
+   */
+  public function add($snippet);
+
+  /**
+   * Update specific properties of a snippet.
+   *
+   * Ex: $region->update('default', ['disabled' => TRUE]);
+   *
+   * @param string $name
+   * @param $snippet
+   * @return static
+   */
+  public function update($name, $snippet);
+
+  /**
+   * Remove all snippets.
+   *
+   * @return static
+   */
+  public function clear();
+
+  /**
+   * Get snippet.
+   *
+   * @param string $name
+   * @return array|NULL
+   */
+  public function &get($name);
+
+  /**
+   * Get a list of all snippets in this collection.
+   *
+   * @return iterable
+   */
+  public function getAll(): iterable;
+
+  /**
+   * Alter the contents of the collection.
+   *
+   * @param callable $callback
+   *   The callback is invoked once for each member in the collection.
+   *   The callback may return one of three values:
+   *   - TRUE: The item is OK and belongs in the collection.
+   *   - FALSE: The item is not OK and should be omitted from the collection.
+   *   - Array: The item should be revised (using the returned value).
+   * @return static
+   */
+  public function filter($callback);
+
+  /**
+   * Find all snippets which match the given criterion.
+   *
+   * @param callable $callback
+   *   The callback is invoked once for each member in the collection.
+   *   The callback may return one of two values:
+   *   - TRUE: The item is OK and belongs in the collection.
+   *   - FALSE: The item is not OK and should be omitted from the collection.
+   * @return iterable
+   *   List of matching snippets.
+   */
+  public function find($callback): iterable;
+
+  /**
+   * Assimilate a list of resources into this list.
+   *
+   * @param iterable $others
+   *   List of snippets to add.
+   * @return static
+   * @see CRM_Core_Resources_CollectionInterface::merge()
+   */
+  public function merge(iterable $others);
+
+}
index b402e61ea797c9bd080d73bb5ee69d83f10f980c..c2adc9e1981baf04053426aa0f9b260c8a0c4236 100644 (file)
@@ -20,6 +20,8 @@
  */
 trait CRM_Core_Resources_CollectionTrait {
 
+  use CRM_Core_Resources_CollectionAdderTrait;
+
   /**
    * Static defaults - a list of options to apply to any new snippets.
    *
@@ -349,233 +351,6 @@ trait CRM_Core_Resources_CollectionTrait {
     return $this->merge($bundle->getAll());
   }
 
-  /**
-   * Export permission data to the client to enable smarter GUIs.
-   *
-   * Note: Application security stems from the server's enforcement
-   * of the security logic (e.g. in the API permissions). There's no way
-   * the client can use this info to make the app more secure; however,
-   * it can produce a better-tuned (non-broken) UI.
-   *
-   * @param string|iterable $permNames
-   *   List of permission names to check/export.
-   * @return static
-   */
-  public function addPermissions($permNames) {
-    // TODO: Maybe this should be its own resource type to allow smarter management?
-    $permNames = is_scalar($permNames) ? [$permNames] : $permNames;
-
-    $perms = [];
-    foreach ($permNames as $permName) {
-      $perms[$permName] = CRM_Core_Permission::check($permName);
-    }
-    return $this->addSetting([
-      'permissions' => $perms,
-    ]);
-  }
-
-  /**
-   * Add a JavaScript file to the current page using <SCRIPT SRC>.
-   *
-   * @param string $code
-   *   JavaScript source code.
-   * @param array $options
-   *   Open-ended list of options (per add())
-   *   Ex: ['weight' => 123]
-   * @return static
-   */
-  public function addScript(string $code, array $options = []) {
-    $this->add($options + ['script' => $code]);
-    return $this;
-  }
-
-  /**
-   * Add a JavaScript file to the current page using <SCRIPT SRC>.
-   *
-   * @param string $ext
-   *   extension name; use 'civicrm' for core.
-   * @param string $file
-   *   file path -- relative to the extension base dir.
-   * @param array $options
-   *   Open-ended list of options (per add()).
-   *   Ex: ['weight' => 123]
-   *   Accepts some additional options:
-   *   - bool|string $translate: Whether to load translated strings for this file. Use one of:
-   *     - FALSE: Do not load translated strings.
-   *     - TRUE: Load translated strings. Use the $ext's default domain.
-   *     - string: Load translated strings. Use a specific domain.
-   *
-   * @return static
-   *
-   * @throws \CRM_Core_Exception
-   */
-  public function addScriptFile(string $ext, string $file, array $options = []) {
-    $this->add($options + ['scriptFile' => [$ext, $file]]);
-    return $this;
-  }
-
-  /**
-   * Add a JavaScript file to the current page using <SCRIPT SRC>.
-   *
-   * @param string $url
-   * @param array $options
-   *   Open-ended list of options (per add())
-   *   Ex: ['weight' => 123]
-   * @return static
-   */
-  public function addScriptUrl(string $url, array $options = []) {
-    $this->add($options + ['scriptUrl' => $url]);
-    return $this;
-  }
-
-  /**
-   * Add translated string to the js CRM object.
-   * It can then be retrived from the client-side ts() function
-   * Variable substitutions can happen from client-side
-   *
-   * Note: this function rarely needs to be called directly and is mostly for internal use.
-   * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
-   *
-   * Simple example:
-   * // From php:
-   * CRM_Core_Resources::singleton()->addString('Hello');
-   * // The string is now available to javascript code i.e.
-   * ts('Hello');
-   *
-   * Example with client-side substitutions:
-   * // From php:
-   * CRM_Core_Resources::singleton()->addString('Your %1 has been %2');
-   * // ts() in javascript works the same as in php, for example:
-   * ts('Your %1 has been %2', {1: objectName, 2: actionTaken});
-   *
-   * NOTE: This function does not work with server-side substitutions
-   * (as this might result in collisions and unwanted variable injections)
-   * Instead, use code like:
-   * CRM_Core_Resources::singleton()->addSetting(array('myNamespace' => array('myString' => ts('Your %1 has been %2', array(subs)))));
-   * And from javascript access it at CRM.myNamespace.myString
-   *
-   * @param string|array $text
-   * @param string|null $domain
-   * @return static
-   */
-  public function addString($text, $domain = 'civicrm') {
-    // TODO: Maybe this should be its own resource type to allow smarter management?
-
-    foreach ((array) $text as $str) {
-      $translated = ts($str, [
-        'domain' => ($domain == 'civicrm') ? NULL : [$domain, NULL],
-        'raw' => TRUE,
-      ]);
-
-      // We only need to push this string to client if the translation
-      // is actually different from the original
-      if ($translated != $str) {
-        $bucket = $domain == 'civicrm' ? 'strings' : 'strings::' . $domain;
-        $this->addSetting([
-          $bucket => [$str => $translated],
-        ]);
-      }
-    }
-    return $this;
-  }
-
-  /**
-   * Add a CSS content to the current page using <STYLE>.
-   *
-   * @param string $code
-   *   CSS source code.
-   * @param array $options
-   *   Open-ended list of options (per add())
-   *   Ex: ['weight' => 123]
-   * @return static
-   */
-  public function addStyle(string $code, array $options = []) {
-    $this->add($options + ['style' => $code]);
-    return $this;
-  }
-
-  /**
-   * Add a CSS file to the current page using <LINK HREF>.
-   *
-   * @param string $ext
-   *   extension name; use 'civicrm' for core.
-   * @param string $file
-   *   file path -- relative to the extension base dir.
-   * @param array $options
-   *   Open-ended list of options (per add())
-   *   Ex: ['weight' => 123]
-   * @return static
-   */
-  public function addStyleFile(string $ext, string $file, array $options = []) {
-    $this->add($options + ['styleFile' => [$ext, $file]]);
-    return $this;
-  }
-
-  /**
-   * Add a CSS file to the current page using <LINK HREF>.
-   *
-   * @param string $url
-   * @param array $options
-   *   Open-ended list of options (per add())
-   *   Ex: ['weight' => 123]
-   * @return static
-   */
-  public function addStyleUrl(string $url, array $options = []) {
-    $this->add($options + ['styleUrl' => $url]);
-    return $this;
-  }
-
-  /**
-   * Add JavaScript variables to CRM.vars
-   *
-   * Example:
-   *   From the server:
-   *     CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
-   *   Access var from javascript:
-   *     CRM.vars.myNamespace.foo // "bar"
-   *
-   * @see https://docs.civicrm.org/dev/en/latest/standards/javascript/
-   *
-   * @param string $nameSpace
-   *   Usually the name of your extension.
-   * @param array $vars
-   * @return static
-   */
-  public function addVars(string $nameSpace, array $vars) {
-    $s = &$this->findCreateSettingSnippet();
-    $s['settings']['vars'][$nameSpace] = $this->mergeSettings(
-      $s['settings']['vars'][$nameSpace] ?? [],
-      $vars
-    );
-    return $this;
-  }
-
-  /**
-   * Add JavaScript variables to the root of the CRM object.
-   * This function is usually reserved for low-level system use.
-   * Extensions and components should generally use addVars instead.
-   *
-   * @param array $settings
-   * @return static
-   */
-  public function addSetting(array $settings) {
-    $s = &$this->findCreateSettingSnippet();
-    $s['settings'] = $this->mergeSettings($s['settings'], $settings);
-    return $this;
-  }
-
-  /**
-   * Add JavaScript variables to the global CRM object via a callback function.
-   *
-   * @param callable $callable
-   * @return static
-   */
-  public function addSettingsFactory($callable) {
-    $s = &$this->findCreateSettingSnippet();
-    $s['settingsFactories'][] = $callable;
-    return $this;
-  }
-
   /**
    * Get a fully-formed/altered list of settings, including the results of
    * any callbacks/listeners.
@@ -586,32 +361,16 @@ trait CRM_Core_Resources_CollectionTrait {
     $s = &$this->findCreateSettingSnippet();
     $result = $s['settings'];
     foreach ($s['settingsFactories'] as $callable) {
-      $result = $this->mergeSettings($result, $callable());
+      $result = CRM_Core_Resources_CollectionAdderTrait::mergeSettings($result, $callable());
     }
     CRM_Utils_Hook::alterResourceSettings($result);
     return $result;
   }
 
-  /**
-   * @param array $settings
-   * @param array $additions
-   * @return array
-   *   combination of $settings and $additions
-   */
-  private function mergeSettings(array $settings, array $additions): array {
-    foreach ($additions as $k => $v) {
-      if (isset($settings[$k]) && is_array($settings[$k]) && is_array($v)) {
-        $v += $settings[$k];
-      }
-      $settings[$k] = $v;
-    }
-    return $settings;
-  }
-
   /**
    * @return array
    */
-  private function &findCreateSettingSnippet(): array {
+  protected function &findCreateSettingSnippet($options = []): array {
     $snippet = &$this->get('settings');
     if ($snippet !== NULL) {
       return $snippet;