Merge pull request #18411 from MegaphoneJon/pcp-wysiwyg
[civicrm-core.git] / CRM / Core / Resources / CollectionTrait.php
CommitLineData
8dbd7691
TO
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 is a building-block for creating classes which maintain a list of resources.
950538ac 16 * It implements of the `CollectionInterface`.
007b7d35 17 *
950538ac 18 * @see CRM_Core_Resources_CollectionInterface
8dbd7691
TO
19 */
20trait CRM_Core_Resources_CollectionTrait {
21
060617e9
TO
22 use CRM_Core_Resources_CollectionAdderTrait;
23
8dbd7691
TO
24 /**
25 * Static defaults - a list of options to apply to any new snippets.
26 *
27 * @var array
28 */
29 protected $defaults = ['weight' => 1, 'disabled' => FALSE];
30
31 /**
32 * List of snippets to inject within region.
33 *
34 * e.g. $this->_snippets[3]['type'] = 'template';
35 *
36 * @var array
37 */
38 protected $snippets = [];
39
40 /**
41 * Whether the snippets array has been sorted
42 *
43 * @var bool
44 */
45 protected $isSorted = TRUE;
46
47 /**
48 * Whitelist of supported types.
49 *
50 * @var array
51 */
52 protected $types = [];
53
54 /**
c8cbd3ba 55 * Add an item to the collection.
8dbd7691
TO
56 *
57 * @param array $snippet
c8cbd3ba 58 * Resource options. See CollectionInterface docs.
8dbd7691
TO
59 * @return array
60 * The full/computed snippet (with defaults applied).
c8cbd3ba
TO
61 * @see CRM_Core_Resources_CollectionInterface
62 * @see CRM_Core_Resources_CollectionInterface::add()
8dbd7691
TO
63 */
64 public function add($snippet) {
65 $snippet = array_merge($this->defaults, $snippet);
8b7abdb6 66 $snippet['id'] = $this->nextId();
8dbd7691
TO
67 if (!isset($snippet['type'])) {
68 foreach ($this->types as $type) {
69 // auto-detect
70 if (isset($snippet[$type])) {
71 $snippet['type'] = $type;
72 break;
73 }
74 }
75 }
bac2c5e4
TO
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");
8dbd7691 79 }
5dddc6e0
TO
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'];
83 }
84 else {
007b7d35
TO
85 switch ($snippet['type']) {
86 case 'scriptUrl':
87 case 'styleUrl':
8b7abdb6 88 $snippet['sortId'] = $snippet['id'];
007b7d35
TO
89 $snippet['name'] = $snippet[$snippet['type']];
90 break;
91
bac2c5e4
TO
92 case 'scriptFile':
93 case 'styleFile':
8b7abdb6 94 $snippet['sortId'] = $snippet['id'];
bac2c5e4
TO
95 $snippet['name'] = implode(':', $snippet[$snippet['type']]);
96 break;
97
007b7d35 98 default:
8b7abdb6 99 $snippet['sortId'] = $snippet['id'];
5dddc6e0 100 $snippet['name'] = $snippet['sortId'];
007b7d35
TO
101 break;
102 }
8dbd7691
TO
103 }
104
bac2c5e4
TO
105 if ($snippet['type'] === 'scriptFile' && !isset($snippet['scriptFileUrls'])) {
106 $res = Civi::resources();
107 list ($ext, $file) = $snippet['scriptFile'];
108
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);
114 }
115 $snippet['scriptFileUrls'] = [$res->getUrl($ext, $res->filterMinify($ext, $file), TRUE)];
116 }
920161ed
TO
117 if ($snippet['type'] === 'scriptFile' && !isset($snippet['aliases'])) {
118 $snippet['aliases'] = $snippet['scriptFileUrls'];
119 }
bac2c5e4
TO
120
121 if ($snippet['type'] === 'styleFile' && !isset($snippet['styleFileUrls'])) {
122 /** @var Civi\Core\Themes $theme */
123 $theme = Civi::service('themes');
124 list ($ext, $file) = $snippet['styleFile'];
125 $snippet['styleFileUrls'] = $theme->resolveUrls($theme->getActiveThemeKey(), $ext, $file);
126 }
920161ed
TO
127 if ($snippet['type'] === 'styleFile' && !isset($snippet['aliases'])) {
128 $snippet['aliases'] = $snippet['styleFileUrls'];
129 }
bac2c5e4 130
67e37261
TO
131 if (isset($snippet['aliases']) && !is_array($snippet['aliases'])) {
132 $snippet['aliases'] = [$snippet['aliases']];
133 }
134
8dbd7691
TO
135 $this->snippets[$snippet['name']] = $snippet;
136 $this->isSorted = FALSE;
137 return $snippet;
138 }
139
5dddc6e0
TO
140 protected function nextId() {
141 if (!isset(Civi::$statics['CRM_Core_Resource_Count'])) {
142 $resId = Civi::$statics['CRM_Core_Resource_Count'] = 1;
143 }
144 else {
145 $resId = ++Civi::$statics['CRM_Core_Resource_Count'];
146 }
147
148 return $resId;
149 }
150
8dbd7691 151 /**
c8cbd3ba
TO
152 * Update specific properties of a snippet.
153 *
8dbd7691 154 * @param string $name
c8cbd3ba
TO
155 * Symbolic of the resource/snippet to update.
156 * @param array $snippet
157 * Resource options. See CollectionInterface docs.
158 * @return static
159 * @see CRM_Core_Resources_CollectionInterface::update()
8dbd7691
TO
160 */
161 public function update($name, $snippet) {
67e37261
TO
162 foreach ($this->resolveName($name) as $realName) {
163 $this->snippets[$realName] = array_merge($this->snippets[$realName], $snippet);
164 $this->isSorted = FALSE;
165 return $this;
166 }
167
168 Civi::log()->warning('Failed to update resource by name ({name})', [
169 'name' => $name,
170 ]);
c8cbd3ba 171 return $this;
8dbd7691
TO
172 }
173
04615f53
TO
174 /**
175 * Remove all snippets.
176 *
177 * @return static
c8cbd3ba 178 * @see CRM_Core_Resources_CollectionInterface::clear()
04615f53
TO
179 */
180 public function clear() {
181 $this->snippets = [];
182 $this->isSorted = TRUE;
183 return $this;
184 }
185
8dbd7691
TO
186 /**
187 * Get snippet.
188 *
189 * @param string $name
190 * @return array|NULL
c8cbd3ba 191 * @see CRM_Core_Resources_CollectionInterface::get()
8dbd7691
TO
192 */
193 public function &get($name) {
67e37261
TO
194 foreach ($this->resolveName($name) as $realName) {
195 return $this->snippets[$realName];
196 }
197
198 $null = NULL;
199 return $null;
8dbd7691
TO
200 }
201
04615f53
TO
202 /**
203 * Get a list of all snippets in this collection.
204 *
205 * @return iterable
c8cbd3ba 206 * @see CRM_Core_Resources_CollectionInterface::getAll()
04615f53
TO
207 */
208 public function getAll(): iterable {
209 $this->sort();
210 return $this->snippets;
211 }
212
213 /**
214 * Alter the contents of the collection.
215 *
216 * @param callable $callback
217 * The callback is invoked once for each member in the collection.
218 * The callback may return one of three values:
219 * - TRUE: The item is OK and belongs in the collection.
220 * - FALSE: The item is not OK and should be omitted from the collection.
221 * - Array: The item should be revised (using the returned value).
222 * @return static
c8cbd3ba 223 * @see CRM_Core_Resources_CollectionInterface::filter()
04615f53
TO
224 */
225 public function filter($callback) {
226 $this->sort();
227 $names = array_keys($this->snippets);
228 foreach ($names as $name) {
229 $ret = $callback($this->snippets[$name]);
230 if ($ret === TRUE) {
231 // OK
232 }
233 elseif ($ret === FALSE) {
234 unset($this->snippets[$name]);
235 }
236 elseif (is_array($ret)) {
237 $this->snippets[$name] = $ret;
238 $this->isSorted = FALSE;
239 }
240 else {
241 throw new \RuntimeException("CollectionTrait::filter() - Callback returned invalid value");
242 }
243 }
244 return $this;
245 }
246
247 /**
248 * Find all snippets which match the given criterion.
249 *
250 * @param callable $callback
c8cbd3ba
TO
251 * The callback is invoked once for each member in the collection.
252 * The callback may return one of three values:
253 * - TRUE: The item is OK and belongs in the collection.
254 * - FALSE: The item is not OK and should be omitted from the collection.
04615f53
TO
255 * @return iterable
256 * List of matching snippets.
c8cbd3ba 257 * @see CRM_Core_Resources_CollectionInterface::find()
04615f53
TO
258 */
259 public function find($callback): iterable {
260 $r = [];
261 $this->sort();
262 foreach ($this->snippets as $name => $snippet) {
263 if ($callback($snippet)) {
264 $r[$name] = $snippet;
265 }
266 }
267 return $r;
268 }
269
5dddc6e0
TO
270 /**
271 * Assimilate a list of resources into this list.
272 *
273 * @param iterable $snippets
274 * List of snippets to add.
275 * @return static
276 * @see CRM_Core_Resources_CollectionInterface::merge()
277 */
278 public function merge(iterable $snippets) {
279 foreach ($snippets as $next) {
280 $name = $next['name'];
281 $current = $this->snippets[$name] ?? NULL;
282 if ($current === NULL) {
283 $this->add($next);
284 }
285 elseif ($current['type'] === 'settings' && $next['type'] === 'settings') {
286 $this->addSetting($next['settings']);
287 foreach ($next['settingsFactories'] as $factory) {
288 $this->addSettingsFactory($factory);
289 }
290 $this->isSorted = FALSE;
291 }
292 elseif ($current['type'] === 'settings' || $next['type'] === 'settings') {
293 throw new \RuntimeException(sprintf("Cannot merge snippets of types [%s] and [%s]" . $current['type'], $next['type']));
294 }
295 else {
296 $this->add($next);
297 }
298 }
299 return $this;
300 }
301
8dbd7691
TO
302 /**
303 * Ensure that the collection is sorted.
304 *
305 * @return static
306 */
307 protected function sort() {
308 if (!$this->isSorted) {
309 uasort($this->snippets, [__CLASS__, '_cmpSnippet']);
310 $this->isSorted = TRUE;
311 }
312 return $this;
313 }
314
67e37261
TO
315 /**
316 * @param string $name
317 * Name or alias.
318 * return array
319 * List of real names.
320 */
321 protected function resolveName($name) {
322 if (isset($this->snippets[$name])) {
323 return [$name];
324 }
325 foreach ($this->snippets as $snippetName => $snippet) {
326 if (isset($snippet['aliases']) && in_array($name, $snippet['aliases'])) {
327 return [$snippetName];
328 }
329 }
330 return [];
331 }
332
8dbd7691
TO
333 /**
334 * @param $a
335 * @param $b
336 *
337 * @return int
338 */
339 public static function _cmpSnippet($a, $b) {
340 if ($a['weight'] < $b['weight']) {
341 return -1;
342 }
343 if ($a['weight'] > $b['weight']) {
344 return 1;
345 }
346 // fallback to name sort; don't really want to do this, but it makes results more stable
5dddc6e0 347 if ($a['sortId'] < $b['sortId']) {
8dbd7691
TO
348 return -1;
349 }
5dddc6e0 350 if ($a['sortId'] > $b['sortId']) {
8dbd7691
TO
351 return 1;
352 }
353 return 0;
354 }
355
007b7d35
TO
356 // -----------------------------------------------
357
fcf926ad
TO
358 /**
359 * Assimilate all the resources listed in a bundle.
360 *
361 * @param iterable|string|\CRM_Core_Resources_Bundle $bundle
362 * Either bundle object, or the symbolic name of a bundle.
363 * Note: For symbolic names, the bundle must be a container service ('bundle.FOO').
364 * @return static
365 */
366 public function addBundle($bundle) {
367 if (is_iterable($bundle)) {
368 foreach ($bundle as $b) {
369 $this->addBundle($b);
fcf926ad 370 }
87edc8d2 371 return $this;
fcf926ad
TO
372 }
373 if (is_string($bundle)) {
374 $bundle = Civi::service('bundle.' . $bundle);
375 }
376 return $this->merge($bundle->getAll());
377 }
378
f55f8f17
TO
379 /**
380 * Get a fully-formed/altered list of settings, including the results of
381 * any callbacks/listeners.
382 *
383 * @return array
384 */
385 public function getSettings(): array {
386 $s = &$this->findCreateSettingSnippet();
387 $result = $s['settings'];
388 foreach ($s['settingsFactories'] as $callable) {
060617e9 389 $result = CRM_Core_Resources_CollectionAdderTrait::mergeSettings($result, $callable());
f55f8f17
TO
390 }
391 CRM_Utils_Hook::alterResourceSettings($result);
392 return $result;
393 }
394
f55f8f17
TO
395 /**
396 * @return array
397 */
e9d08c6b 398 public function &findCreateSettingSnippet($options = []): array {
f55f8f17
TO
399 $snippet = &$this->get('settings');
400 if ($snippet !== NULL) {
401 return $snippet;
402 }
403
404 $this->add([
405 'name' => 'settings',
406 'type' => 'settings',
407 'settings' => [],
408 'settingsFactories' => [],
409 'weight' => -100000,
410 ]);
411 return $this->get('settings');
412 }
413
8dbd7691 414}