Merge pull request #18753 from agh1/placeholderfix-5.31
[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 }
117
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);
123 }
124
8dbd7691
TO
125 $this->snippets[$snippet['name']] = $snippet;
126 $this->isSorted = FALSE;
127 return $snippet;
128 }
129
5dddc6e0
TO
130 protected function nextId() {
131 if (!isset(Civi::$statics['CRM_Core_Resource_Count'])) {
132 $resId = Civi::$statics['CRM_Core_Resource_Count'] = 1;
133 }
134 else {
135 $resId = ++Civi::$statics['CRM_Core_Resource_Count'];
136 }
137
138 return $resId;
139 }
140
8dbd7691 141 /**
c8cbd3ba
TO
142 * Update specific properties of a snippet.
143 *
8dbd7691 144 * @param string $name
c8cbd3ba
TO
145 * Symbolic of the resource/snippet to update.
146 * @param array $snippet
147 * Resource options. See CollectionInterface docs.
148 * @return static
149 * @see CRM_Core_Resources_CollectionInterface::update()
8dbd7691
TO
150 */
151 public function update($name, $snippet) {
152 $this->snippets[$name] = array_merge($this->snippets[$name], $snippet);
153 $this->isSorted = FALSE;
c8cbd3ba 154 return $this;
8dbd7691
TO
155 }
156
04615f53
TO
157 /**
158 * Remove all snippets.
159 *
160 * @return static
c8cbd3ba 161 * @see CRM_Core_Resources_CollectionInterface::clear()
04615f53
TO
162 */
163 public function clear() {
164 $this->snippets = [];
165 $this->isSorted = TRUE;
166 return $this;
167 }
168
8dbd7691
TO
169 /**
170 * Get snippet.
171 *
172 * @param string $name
173 * @return array|NULL
c8cbd3ba 174 * @see CRM_Core_Resources_CollectionInterface::get()
8dbd7691
TO
175 */
176 public function &get($name) {
177 return $this->snippets[$name];
178 }
179
04615f53
TO
180 /**
181 * Get a list of all snippets in this collection.
182 *
183 * @return iterable
c8cbd3ba 184 * @see CRM_Core_Resources_CollectionInterface::getAll()
04615f53
TO
185 */
186 public function getAll(): iterable {
187 $this->sort();
188 return $this->snippets;
189 }
190
191 /**
192 * Alter the contents of the collection.
193 *
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).
200 * @return static
c8cbd3ba 201 * @see CRM_Core_Resources_CollectionInterface::filter()
04615f53
TO
202 */
203 public function filter($callback) {
204 $this->sort();
205 $names = array_keys($this->snippets);
206 foreach ($names as $name) {
207 $ret = $callback($this->snippets[$name]);
208 if ($ret === TRUE) {
209 // OK
210 }
211 elseif ($ret === FALSE) {
212 unset($this->snippets[$name]);
213 }
214 elseif (is_array($ret)) {
215 $this->snippets[$name] = $ret;
216 $this->isSorted = FALSE;
217 }
218 else {
219 throw new \RuntimeException("CollectionTrait::filter() - Callback returned invalid value");
220 }
221 }
222 return $this;
223 }
224
225 /**
226 * Find all snippets which match the given criterion.
227 *
228 * @param callable $callback
c8cbd3ba
TO
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.
04615f53
TO
233 * @return iterable
234 * List of matching snippets.
c8cbd3ba 235 * @see CRM_Core_Resources_CollectionInterface::find()
04615f53
TO
236 */
237 public function find($callback): iterable {
238 $r = [];
239 $this->sort();
240 foreach ($this->snippets as $name => $snippet) {
241 if ($callback($snippet)) {
242 $r[$name] = $snippet;
243 }
244 }
245 return $r;
246 }
247
5dddc6e0
TO
248 /**
249 * Assimilate a list of resources into this list.
250 *
251 * @param iterable $snippets
252 * List of snippets to add.
253 * @return static
254 * @see CRM_Core_Resources_CollectionInterface::merge()
255 */
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) {
261 $this->add($next);
262 }
263 elseif ($current['type'] === 'settings' && $next['type'] === 'settings') {
264 $this->addSetting($next['settings']);
265 foreach ($next['settingsFactories'] as $factory) {
266 $this->addSettingsFactory($factory);
267 }
268 $this->isSorted = FALSE;
269 }
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']));
272 }
273 else {
274 $this->add($next);
275 }
276 }
277 return $this;
278 }
279
8dbd7691
TO
280 /**
281 * Ensure that the collection is sorted.
282 *
283 * @return static
284 */
285 protected function sort() {
286 if (!$this->isSorted) {
287 uasort($this->snippets, [__CLASS__, '_cmpSnippet']);
288 $this->isSorted = TRUE;
289 }
290 return $this;
291 }
292
293 /**
294 * @param $a
295 * @param $b
296 *
297 * @return int
298 */
299 public static function _cmpSnippet($a, $b) {
300 if ($a['weight'] < $b['weight']) {
301 return -1;
302 }
303 if ($a['weight'] > $b['weight']) {
304 return 1;
305 }
306 // fallback to name sort; don't really want to do this, but it makes results more stable
5dddc6e0 307 if ($a['sortId'] < $b['sortId']) {
8dbd7691
TO
308 return -1;
309 }
5dddc6e0 310 if ($a['sortId'] > $b['sortId']) {
8dbd7691
TO
311 return 1;
312 }
313 return 0;
314 }
315
007b7d35
TO
316 // -----------------------------------------------
317
fcf926ad
TO
318 /**
319 * Assimilate all the resources listed in a bundle.
320 *
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').
324 * @return static
325 */
326 public function addBundle($bundle) {
327 if (is_iterable($bundle)) {
328 foreach ($bundle as $b) {
329 $this->addBundle($b);
fcf926ad 330 }
87edc8d2 331 return $this;
fcf926ad
TO
332 }
333 if (is_string($bundle)) {
334 $bundle = Civi::service('bundle.' . $bundle);
335 }
336 return $this->merge($bundle->getAll());
337 }
338
f55f8f17
TO
339 /**
340 * Get a fully-formed/altered list of settings, including the results of
341 * any callbacks/listeners.
342 *
343 * @return array
344 */
345 public function getSettings(): array {
346 $s = &$this->findCreateSettingSnippet();
347 $result = $s['settings'];
348 foreach ($s['settingsFactories'] as $callable) {
060617e9 349 $result = CRM_Core_Resources_CollectionAdderTrait::mergeSettings($result, $callable());
f55f8f17
TO
350 }
351 CRM_Utils_Hook::alterResourceSettings($result);
352 return $result;
353 }
354
f55f8f17
TO
355 /**
356 * @return array
357 */
e9d08c6b 358 public function &findCreateSettingSnippet($options = []): array {
f55f8f17
TO
359 $snippet = &$this->get('settings');
360 if ($snippet !== NULL) {
361 return $snippet;
362 }
363
364 $this->add([
365 'name' => 'settings',
366 'type' => 'settings',
367 'settings' => [],
368 'settingsFactories' => [],
369 'weight' => -100000,
370 ]);
371 return $this->get('settings');
372 }
373
8dbd7691 374}