3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * Class CRM_Core_Resources_CollectionTrait
15 * This is a building-block for creating classes which maintain a list of resources.
16 * It implements of the `CollectionInterface`.
18 * @see CRM_Core_Resources_CollectionInterface
20 trait CRM_Core_Resources_CollectionTrait
{
22 use CRM_Core_Resources_CollectionAdderTrait
;
25 * Static defaults - a list of options to apply to any new snippets.
29 protected $defaults = ['weight' => 1, 'disabled' => FALSE];
32 * List of snippets to inject within region.
34 * e.g. $this->_snippets[3]['type'] = 'template';
38 protected $snippets = [];
41 * Whether the snippets array has been sorted
45 protected $isSorted = TRUE;
48 * Whitelist of supported types.
52 protected $types = [];
55 * Add an item to the collection.
57 * @param array $snippet
58 * Resource options. See CollectionInterface docs.
60 * The full/computed snippet (with defaults applied).
61 * @see CRM_Core_Resources_CollectionInterface
62 * @see CRM_Core_Resources_CollectionInterface::add()
64 public function add($snippet) {
65 $snippet = array_merge($this->defaults
, $snippet);
66 $snippet['id'] = $this->nextId();
67 if (!isset($snippet['type'])) {
68 foreach ($this->types
as $type) {
70 if (isset($snippet[$type])) {
71 $snippet['type'] = $type;
76 if (!in_array($snippet['type'] ??
NULL, $this->types
)) {
77 $typeExpr = $snippet['type'] ??
'(' . implode(',', array_keys($snippet)) . ')';
78 throw new \
RuntimeException("Unsupported snippet type: $typeExpr");
80 // Traditional behavior: sort by (1) weight and (2) either name or natural position. This second thing is called 'sortId'.
81 if (isset($snippet['name'])) {
82 $snippet['sortId'] = $snippet['name'];
85 switch ($snippet['type']) {
88 $snippet['sortId'] = $snippet['id'];
89 $snippet['name'] = $snippet[$snippet['type']];
94 $snippet['sortId'] = $snippet['id'];
95 $snippet['name'] = implode(':', $snippet[$snippet['type']]);
99 $snippet['sortId'] = $snippet['id'];
100 $snippet['name'] = $snippet['sortId'];
105 if ($snippet['type'] === 'scriptFile' && !isset($snippet['scriptFileUrls'])) {
106 $res = Civi
::resources();
107 list ($ext, $file) = $snippet['scriptFile'];
109 $snippet['translate'] = $snippet['translate'] ??
TRUE;
110 if ($snippet['translate']) {
111 $domain = ($snippet['translate'] === TRUE) ?
$ext : $snippet['translate'];
112 // Is this too early?
113 $this->addString(Civi
::service('resources.js_strings')->get($domain, $res->getPath($ext, $file), 'text/javascript'), $domain);
115 $snippet['scriptFileUrls'] = [$res->getUrl($ext, $res->filterMinify($ext, $file), TRUE)];
118 if ($snippet['type'] === 'styleFile' && !isset($snippet['styleFileUrls'])) {
119 /** @var Civi\Core\Themes $theme */
120 $theme = Civi
::service('themes');
121 list ($ext, $file) = $snippet['styleFile'];
122 $snippet['styleFileUrls'] = $theme->resolveUrls($theme->getActiveThemeKey(), $ext, $file);
125 $this->snippets
[$snippet['name']] = $snippet;
126 $this->isSorted
= FALSE;
130 protected function nextId() {
131 if (!isset(Civi
::$statics['CRM_Core_Resource_Count'])) {
132 $resId = Civi
::$statics['CRM_Core_Resource_Count'] = 1;
135 $resId = ++Civi
::$statics['CRM_Core_Resource_Count'];
142 * Update specific properties of a snippet.
144 * @param string $name
145 * Symbolic of the resource/snippet to update.
146 * @param array $snippet
147 * Resource options. See CollectionInterface docs.
149 * @see CRM_Core_Resources_CollectionInterface::update()
151 public function update($name, $snippet) {
152 $this->snippets
[$name] = array_merge($this->snippets
[$name], $snippet);
153 $this->isSorted
= FALSE;
158 * Remove all snippets.
161 * @see CRM_Core_Resources_CollectionInterface::clear()
163 public function clear() {
164 $this->snippets
= [];
165 $this->isSorted
= TRUE;
172 * @param string $name
174 * @see CRM_Core_Resources_CollectionInterface::get()
176 public function &get($name) {
177 return $this->snippets
[$name];
181 * Get a list of all snippets in this collection.
184 * @see CRM_Core_Resources_CollectionInterface::getAll()
186 public function getAll(): iterable
{
188 return $this->snippets
;
192 * Alter the contents of the collection.
194 * @param callable $callback
195 * The callback is invoked once for each member in the collection.
196 * The callback may return one of three values:
197 * - TRUE: The item is OK and belongs in the collection.
198 * - FALSE: The item is not OK and should be omitted from the collection.
199 * - Array: The item should be revised (using the returned value).
201 * @see CRM_Core_Resources_CollectionInterface::filter()
203 public function filter($callback) {
205 $names = array_keys($this->snippets
);
206 foreach ($names as $name) {
207 $ret = $callback($this->snippets
[$name]);
211 elseif ($ret === FALSE) {
212 unset($this->snippets
[$name]);
214 elseif (is_array($ret)) {
215 $this->snippets
[$name] = $ret;
216 $this->isSorted
= FALSE;
219 throw new \
RuntimeException("CollectionTrait::filter() - Callback returned invalid value");
226 * Find all snippets which match the given criterion.
228 * @param callable $callback
229 * The callback is invoked once for each member in the collection.
230 * The callback may return one of three values:
231 * - TRUE: The item is OK and belongs in the collection.
232 * - FALSE: The item is not OK and should be omitted from the collection.
234 * List of matching snippets.
235 * @see CRM_Core_Resources_CollectionInterface::find()
237 public function find($callback): iterable
{
240 foreach ($this->snippets
as $name => $snippet) {
241 if ($callback($snippet)) {
242 $r[$name] = $snippet;
249 * Assimilate a list of resources into this list.
251 * @param iterable $snippets
252 * List of snippets to add.
254 * @see CRM_Core_Resources_CollectionInterface::merge()
256 public function merge(iterable
$snippets) {
257 foreach ($snippets as $next) {
258 $name = $next['name'];
259 $current = $this->snippets
[$name] ??
NULL;
260 if ($current === NULL) {
263 elseif ($current['type'] === 'settings' && $next['type'] === 'settings') {
264 $this->addSetting($next['settings']);
265 foreach ($next['settingsFactories'] as $factory) {
266 $this->addSettingsFactory($factory);
268 $this->isSorted
= FALSE;
270 elseif ($current['type'] === 'settings' ||
$next['type'] === 'settings') {
271 throw new \
RuntimeException(sprintf("Cannot merge snippets of types [%s] and [%s]" . $current['type'], $next['type']));
281 * Ensure that the collection is sorted.
285 protected function sort() {
286 if (!$this->isSorted
) {
287 uasort($this->snippets
, [__CLASS__
, '_cmpSnippet']);
288 $this->isSorted
= TRUE;
299 public static function _cmpSnippet($a, $b) {
300 if ($a['weight'] < $b['weight']) {
303 if ($a['weight'] > $b['weight']) {
306 // fallback to name sort; don't really want to do this, but it makes results more stable
307 if ($a['sortId'] < $b['sortId']) {
310 if ($a['sortId'] > $b['sortId']) {
316 // -----------------------------------------------
319 * Assimilate all the resources listed in a bundle.
321 * @param iterable|string|\CRM_Core_Resources_Bundle $bundle
322 * Either bundle object, or the symbolic name of a bundle.
323 * Note: For symbolic names, the bundle must be a container service ('bundle.FOO').
326 public function addBundle($bundle) {
327 if (is_iterable($bundle)) {
328 foreach ($bundle as $b) {
329 $this->addBundle($b);
333 if (is_string($bundle)) {
334 $bundle = Civi
::service('bundle.' . $bundle);
336 return $this->merge($bundle->getAll());
340 * Get a fully-formed/altered list of settings, including the results of
341 * any callbacks/listeners.
345 public function getSettings(): array {
346 $s = &$this->findCreateSettingSnippet();
347 $result = $s['settings'];
348 foreach ($s['settingsFactories'] as $callable) {
349 $result = CRM_Core_Resources_CollectionAdderTrait
::mergeSettings($result, $callable());
351 CRM_Utils_Hook
::alterResourceSettings($result);
358 public function &findCreateSettingSnippet($options = []): array {
359 $snippet = &$this->get('settings');
360 if ($snippet !== NULL) {
365 'name' => 'settings',
366 'type' => 'settings',
368 'settingsFactories' => [],
371 return $this->get('settings');