(REF) CRM_Core_Resources::addBundle() - Improve readability. Add test.
[civicrm-core.git] / CRM / Core / Resources / CollectionAdderTrait.php
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 * Class CRM_Core_Resources_CollectionTrait
14 *
15 * This trait is a building-block for creating classes which maintain a list of
16 * resources. It defines a set of helper functions which provide syntactic sugar
17 * for calling the add() method. It implements most of the `CollectionAdderInterface`.
18 *
19 * @see CRM_Core_Resources_CollectionAdderInterface
20 */
21 trait CRM_Core_Resources_CollectionAdderTrait {
22
23 /**
24 * Add an item to the collection.
25 *
26 * @param array $snippet
27 * @return array
28 * The full/computed snippet (with defaults applied).
29 * @see CRM_Core_Resources_CollectionInterface::add()
30 * @see CRM_Core_Resources_CollectionTrait::add()
31 */
32 abstract public function add($snippet);
33
34 /**
35 * Locate the 'settings' snippet.
36 *
37 * @param array $options
38 * @return array
39 * @see CRM_Core_Resources_CollectionTrait::findCreateSettingSnippet()
40 */
41 abstract public function &findCreateSettingSnippet($options = []): array;
42
43 /**
44 * Export permission data to the client to enable smarter GUIs.
45 *
46 * @param string|iterable $permNames
47 * List of permission names to check/export.
48 * @return static
49 * @see CRM_Core_Resources_CollectionAdderInterface::addPermissions()
50 */
51 public function addPermissions($permNames) {
52 // TODO: Maybe this should be its own resource type to allow smarter management?
53 $permNames = is_scalar($permNames) ? [$permNames] : $permNames;
54
55 $perms = [];
56 foreach ($permNames as $permName) {
57 $perms[$permName] = CRM_Core_Permission::check($permName);
58 }
59 return $this->addSetting([
60 'permissions' => $perms,
61 ]);
62 }
63
64 /**
65 * Add a JavaScript file to the current page using <SCRIPT SRC>.
66 *
67 * Ex: addScript('alert("Hello world");', ['weight' => 123]);
68 *
69 * @param string $code
70 * JavaScript source code.
71 * @param array $options
72 * Open-ended list of key-value options. See CollectionInterface docs.
73 * Positional equivalence: addScript(string $code, int $weight, string $region).
74 * @return static
75 * @see CRM_Core_Resources_CollectionInterface
76 * @see CRM_Core_Resources_CollectionAdderInterface::addScript()
77 */
78 public function addScript(string $code, ...$options) {
79 $this->add(self::mergeStandardOptions($options, [
80 'script' => $code,
81 ]));
82 return $this;
83 }
84
85 /**
86 * Add a JavaScript file to the current page using <SCRIPT SRC>.
87 *
88 * Ex: addScriptFile('myextension', 'myscript.js', ['weight' => 123]);
89 *
90 * @param string $ext
91 * Extension name; use 'civicrm' for core.
92 * @param string $file
93 * File path -- relative to the extension base dir.
94 * @param array $options
95 * Open-ended list of key-value options. See CollectionInterface docs.
96 * Positional equivalence: addScriptFile(string $code, int $weight, string $region, mixed $translate).
97 * @return static
98 * @see CRM_Core_Resources_CollectionInterface
99 * @see CRM_Core_Resources_CollectionAdderInterface::addScriptFile()
100 */
101 public function addScriptFile(string $ext, string $file, ...$options) {
102 $this->add(self::mergeStandardOptions($options, [
103 'scriptFile' => [$ext, $file],
104 'name' => "$ext:$file",
105 // Setting the name above may appear superfluous, but it preserves a historical quirk
106 // where Region::add() and Resources::addScriptFile() produce slightly different orderings.
107 ]));
108 return $this;
109 }
110
111 /**
112 * Add a JavaScript URL to the current page using <SCRIPT SRC>.
113 *
114 * Ex: addScriptUrl('http://example.com/foo.js', ['weight' => 123])
115 *
116 * @param string $url
117 * @param array $options
118 * Open-ended list of key-value options. See CollectionInterface docs.
119 * Positional equivalence: addScriptUrl(string $url, int $weight, string $region).
120 * @return static
121 * @see CRM_Core_Resources_CollectionInterface
122 * @see CRM_Core_Resources_CollectionAdderInterface::addScriptUrl()
123 */
124 public function addScriptUrl(string $url, ...$options) {
125 $this->add(self::mergeStandardOptions($options, [
126 'scriptUrl' => $url,
127 'name' => $url,
128 // Setting the name above may appear superfluous, but it preserves a historical quirk
129 // where Region::add() and Resources::addScriptUrl() produce slightly different orderings.
130 ]));
131 return $this;
132 }
133
134 /**
135 * Add translated string to the js CRM object.
136 * It can then be retrived from the client-side ts() function
137 * Variable substitutions can happen from client-side
138 *
139 * Note: this function rarely needs to be called directly and is mostly for internal use.
140 * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
141 *
142 * Simple example:
143 * // From php:
144 * CRM_Core_Resources::singleton()->addString('Hello');
145 * // The string is now available to javascript code i.e.
146 * ts('Hello');
147 *
148 * Example with client-side substitutions:
149 * // From php:
150 * CRM_Core_Resources::singleton()->addString('Your %1 has been %2');
151 * // ts() in javascript works the same as in php, for example:
152 * ts('Your %1 has been %2', {1: objectName, 2: actionTaken});
153 *
154 * NOTE: This function does not work with server-side substitutions
155 * (as this might result in collisions and unwanted variable injections)
156 * Instead, use code like:
157 * CRM_Core_Resources::singleton()->addSetting(array('myNamespace' => array('myString' => ts('Your %1 has been %2', array(subs)))));
158 * And from javascript access it at CRM.myNamespace.myString
159 *
160 * @param string|array $text
161 * @param string|null $domain
162 * @return static
163 * @see CRM_Core_Resources_CollectionAdderInterface::addString()
164 */
165 public function addString($text, $domain = 'civicrm') {
166 // TODO: Maybe this should be its own resource type to allow smarter management?
167
168 foreach ((array) $text as $str) {
169 $translated = ts($str, [
170 'domain' => ($domain == 'civicrm') ? NULL : [$domain, NULL],
171 'raw' => TRUE,
172 ]);
173
174 // We only need to push this string to client if the translation
175 // is actually different from the original
176 if ($translated != $str) {
177 $bucket = $domain == 'civicrm' ? 'strings' : 'strings::' . $domain;
178 $this->addSetting([
179 $bucket => [$str => $translated],
180 ]);
181 }
182 }
183 return $this;
184 }
185
186 /**
187 * Add a CSS content to the current page using <STYLE>.
188 *
189 * Ex: addStyle('p { color: red; }', ['weight' => 100]);
190 *
191 * @param string $code
192 * CSS source code.
193 * @param array $options
194 * Open-ended list of key-value options. See CollectionInterface docs.
195 * Positional equivalence: addStyle(string $code, int $weight, string $region).
196 * @return static
197 * @see CRM_Core_Resources_CollectionInterface
198 * @see CRM_Core_Resources_CollectionAdderInterface::addStyle()
199 */
200 public function addStyle(string $code, ...$options) {
201 $this->add(self::mergeStandardOptions($options, [
202 'style' => $code,
203 ]));
204 return $this;
205 }
206
207 /**
208 * Add a CSS file to the current page using <LINK HREF>.
209 *
210 * Ex: addStyleFile('myextension', 'mystyles.css', ['weight' => 100]);
211 *
212 * @param string $ext
213 * Extension name; use 'civicrm' for core.
214 * @param string $file
215 * File path -- relative to the extension base dir.
216 * @param array $options
217 * Open-ended list of key-value options. See CollectionInterface docs.
218 * Positional equivalence: addStyle(string $code, int $weight, string $region).
219 * @return static
220 * @see CRM_Core_Resources_CollectionInterface
221 * @see CRM_Core_Resources_CollectionAdderInterface::addStyleFile()
222 */
223 public function addStyleFile(string $ext, string $file, ...$options) {
224 $this->add(self::mergeStandardOptions($options, [
225 'styleFile' => [$ext, $file],
226 'name' => "$ext:$file",
227 // Setting the name above may appear superfluous, but it preserves a historical quirk
228 // where Region::add() and Resources::addScriptUrl() produce slightly different orderings.
229 ]));
230 return $this;
231 }
232
233 /**
234 * Add a CSS file to the current page using <LINK HREF>.
235 *
236 * Ex: addStyleUrl('http://example.com/foo.css', ['weight' => 100]);
237 *
238 * @param string $url
239 * @param array $options
240 * Open-ended list of key-value options. See CollectionInterface docs.
241 * Positional equivalence: addStyleUrl(string $code, int $weight, string $region).
242 * @return static
243 * @see CRM_Core_Resources_CollectionInterface
244 * @see CRM_Core_Resources_CollectionAdderInterface::addStyleUrl()
245 */
246 public function addStyleUrl(string $url, ...$options) {
247 $this->add(self::mergeStandardOptions($options, [
248 'styleUrl' => $url,
249 'name' => $url,
250 // Setting the name above may appear superfluous, but it preserves a historical quirk
251 // where Region::add() and Resources::addScriptUrl() produce slightly different orderings.
252 ]));
253 return $this;
254 }
255
256 /**
257 * Add JavaScript variables to the root of the CRM object.
258 * This function is usually reserved for low-level system use.
259 * Extensions and components should generally use addVars instead.
260 *
261 * @param array $settings
262 * Data to export.
263 * @param array $options
264 * Not used.
265 * Positional equivalence: addSetting(array $settings, string $region).
266 * @return static
267 * @see CRM_Core_Resources_CollectionInterface
268 * @see CRM_Core_Resources_CollectionAdderInterface::addSetting()
269 */
270 public function addSetting(array $settings, ...$options) {
271 $s = &$this->findCreateSettingSnippet($options);
272 $s['settings'] = self::mergeSettings($s['settings'], $settings);
273 return $this;
274 }
275
276 /**
277 * Add JavaScript variables to the global CRM object via a callback function.
278 *
279 * @param callable $callable
280 * @return static
281 * @see CRM_Core_Resources_CollectionAdderInterface::addSettingsFactory()
282 */
283 public function addSettingsFactory($callable) {
284 $s = &$this->findCreateSettingSnippet();
285 $s['settingsFactories'][] = $callable;
286 return $this;
287 }
288
289 /**
290 * Add JavaScript variables to CRM.vars
291 *
292 * Example:
293 * From the server:
294 * CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
295 * Access var from javascript:
296 * CRM.vars.myNamespace.foo // "bar"
297 *
298 * @see https://docs.civicrm.org/dev/en/latest/standards/javascript/
299 *
300 * @param string $nameSpace
301 * Usually the name of your extension.
302 * @param array $vars
303 * Data to export.
304 * @param array $options
305 * Open-ended list of key-value options. See CollectionInterface docs.
306 * Positional equivalence: addVars(string $namespace, array $vars, string $region).
307 * @return static
308 * @see CRM_Core_Resources_CollectionInterface
309 * @see CRM_Core_Resources_CollectionAdderInterface::addVars()
310 */
311 public function addVars(string $nameSpace, array $vars, ...$options) {
312 $s = &$this->findCreateSettingSnippet($options);
313 $s['settings']['vars'][$nameSpace] = self::mergeSettings(
314 $s['settings']['vars'][$nameSpace] ?? [],
315 $vars
316 );
317 return $this;
318 }
319
320 /**
321 * Given the "$options" for "addScriptUrl()" (etal), normalize the contents
322 * and potentially add more.
323 *
324 * @param array $splats
325 * A list of options, as represented by the splat mechanism ("...$options").
326 * This may appear in one of two ways:
327 * - New (String Index): as in `addFoo($foo, array $options)`
328 * - Old (Numeric Index): as in `addFoo($foo, int $weight = X, string $region = Y, bool $translate = X)`
329 * @param array $defaults
330 * List of values to merge into $options.
331 * @return array
332 */
333 public static function mergeStandardOptions(array $splats, array $defaults = []) {
334 $count = count($splats);
335 switch ($count) {
336 case 0:
337 // Common+simple case: No splat options. We can short-circuit.
338 return $defaults;
339
340 case 1:
341 // Might be new format (key-value pairs) or old format
342 $parsed = is_array($splats[0]) ? $splats[0] : ['weight' => $splats[0]];
343 break;
344
345 case 2:
346 $parsed = ['weight' => $splats[0], 'region' => $splats[1]];
347 break;
348
349 case 3:
350 $parsed = ['weight' => $splats[0], 'region' => $splats[1], 'translate' => $splats[2]];
351 break;
352
353 default:
354 throw new \RuntimeException("Cannot resolve resource options. For clearest behavior, pass options in key-value format.");
355 }
356
357 return array_merge($defaults, $parsed);
358 }
359
360 /**
361 * Given the "$options" for "addSetting()" (etal), normalize the contents
362 * and potentially add more.
363 *
364 * @param array $splats
365 * A list of options, as represented by the splat mechanism ("...$options").
366 * This may appear in one of two ways:
367 * - New (String Index): as in `addFoo($foo, array $options)`
368 * - Old (Numeric Index): as in `addFoo($foo, int $weight = X, string $region = Y, bool $translate = X)`
369 * @param array $defaults
370 * List of values to merge into $options.
371 * @return array
372 */
373 public static function mergeSettingOptions(array $splats, array $defaults = []) {
374 $count = count($splats);
375 switch ($count) {
376 case 0:
377 // Common+simple case: No splat options. We can short-circuit.
378 return $defaults;
379
380 case 1:
381 // Might be new format (key-value pairs) or old format
382 $parsed = is_array($splats[0]) ? $splats[0] : ['region' => $splats[0]];
383 break;
384
385 default:
386 throw new \RuntimeException("Cannot resolve resource options. For clearest behavior, pass options in key-value format.");
387 }
388
389 return array_merge($defaults, $parsed);
390 }
391
392 /**
393 * @param array $settings
394 * @param array $additions
395 * @return array
396 * combination of $settings and $additions
397 */
398 public static function mergeSettings(array $settings, array $additions): array {
399 foreach ($additions as $k => $v) {
400 if (isset($settings[$k]) && is_array($settings[$k]) && is_array($v)) {
401 $v += $settings[$k];
402 }
403 $settings[$k] = $v;
404 }
405 return $settings;
406 }
407
408 }