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