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 | } | |
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 | } |