Commit | Line | Data |
---|---|---|
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 | */ |
20 | trait 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 | } |