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