Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.95% |
94 / 95 |
|
91.67% |
11 / 12 |
CRAP | |
0.00% |
0 / 1 |
ThemeInfo | |
98.95% |
94 / 95 |
|
91.67% |
11 / 12 |
38 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBaseDir | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMixinConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getThemeConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTheme | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getTheme | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadThemeConfig | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getThemeInfo | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getMergedConfig | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
10 | |||
findContainingTheme | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
8 | |||
findInThemes | |
96.43% |
27 / 28 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * Class to represent currently-selected theme and related information. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010-2023. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Theme |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Site |
28 | */ |
29 | |
30 | namespace VuFindTheme; |
31 | |
32 | use Laminas\Cache\Storage\StorageInterface; |
33 | use Webmozart\Glob\Glob; |
34 | |
35 | use function is_array; |
36 | use function strlen; |
37 | |
38 | /** |
39 | * Class to represent currently-selected theme and related information. |
40 | * |
41 | * @category VuFind |
42 | * @package Theme |
43 | * @author Demian Katz <demian.katz@villanova.edu> |
44 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
45 | * @link https://vufind.org Main Site |
46 | */ |
47 | class ThemeInfo |
48 | { |
49 | use \VuFind\Feature\MergeRecursiveTrait; |
50 | |
51 | /** |
52 | * Base directory for theme files |
53 | * |
54 | * @var string |
55 | */ |
56 | protected $baseDir; |
57 | |
58 | /** |
59 | * Current selected theme |
60 | * |
61 | * @var string |
62 | */ |
63 | protected $currentTheme; |
64 | |
65 | /** |
66 | * A safe theme (guaranteed to exist) that can be loaded if an invalid |
67 | * configuration is passed in |
68 | * |
69 | * @var string |
70 | */ |
71 | protected $safeTheme; |
72 | |
73 | /** |
74 | * Theme configuration cache |
75 | * |
76 | * @var array |
77 | */ |
78 | protected $allThemeInfo = null; |
79 | |
80 | /** |
81 | * Cache for merged configs |
82 | * |
83 | * @var StorageInterface |
84 | */ |
85 | protected $cache = null; |
86 | |
87 | // Constant for use with findContainingTheme: |
88 | public const RETURN_ALL_DETAILS = 'all'; |
89 | |
90 | /** |
91 | * Constructor |
92 | * |
93 | * @param string $baseDir Base directory for theme files. |
94 | * @param string $safeTheme Theme that should be guaranteed to exist. |
95 | */ |
96 | public function __construct($baseDir, $safeTheme) |
97 | { |
98 | $this->baseDir = $baseDir; |
99 | $this->currentTheme = $this->safeTheme = $safeTheme; |
100 | } |
101 | |
102 | /** |
103 | * Provide cache and activate info caching |
104 | * |
105 | * @param StorageInterface $cache cache object |
106 | * |
107 | * @return void |
108 | */ |
109 | public function setCache(StorageInterface $cache) |
110 | { |
111 | $this->cache = $cache; |
112 | } |
113 | |
114 | /** |
115 | * Get the base directory for themes. |
116 | * |
117 | * @return string |
118 | */ |
119 | public function getBaseDir() |
120 | { |
121 | return $this->baseDir; |
122 | } |
123 | |
124 | /** |
125 | * Get the configuration file for the specified mixin. |
126 | * |
127 | * @param string $mixin Mixin name |
128 | * |
129 | * @return string |
130 | */ |
131 | protected function getMixinConfig($mixin) |
132 | { |
133 | return $this->baseDir . "/$mixin/mixin.config.php"; |
134 | } |
135 | |
136 | /** |
137 | * Get the configuration file for the specified theme. |
138 | * |
139 | * @param string $theme Theme name |
140 | * |
141 | * @return string |
142 | */ |
143 | protected function getThemeConfig($theme) |
144 | { |
145 | return $this->baseDir . "/$theme/theme.config.php"; |
146 | } |
147 | |
148 | /** |
149 | * Set the current theme. |
150 | * |
151 | * @param string $theme Theme to set. |
152 | * |
153 | * @return void |
154 | * @throws \Exception |
155 | */ |
156 | public function setTheme($theme) |
157 | { |
158 | // If the configured theme setting is illegal, throw an exception without |
159 | // making any changes. |
160 | if (!file_exists($this->getThemeConfig($theme))) { |
161 | throw new \Exception('Cannot load theme: ' . $theme); |
162 | } |
163 | if ($theme != $this->currentTheme) { |
164 | // Clear any cached theme information when we change themes: |
165 | $this->allThemeInfo = null; |
166 | $this->currentTheme = $theme; |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Get the current theme. |
172 | * |
173 | * @return string |
174 | */ |
175 | public function getTheme() |
176 | { |
177 | return $this->currentTheme; |
178 | } |
179 | |
180 | /** |
181 | * Load configuration for the specified theme (and its mixins, if any) into the |
182 | * allThemeInfo property. |
183 | * |
184 | * @param string $theme Name of theme to load |
185 | * |
186 | * @return void |
187 | */ |
188 | protected function loadThemeConfig($theme) |
189 | { |
190 | // Load theme configuration... |
191 | $this->allThemeInfo[$theme] = include $this->getThemeConfig($theme); |
192 | // ..and if there are mixins, load those too! |
193 | if (isset($this->allThemeInfo[$theme]['mixins'])) { |
194 | foreach ($this->allThemeInfo[$theme]['mixins'] as $mix) { |
195 | $this->allThemeInfo[$mix] = include $this->getMixinConfig($mix); |
196 | } |
197 | } |
198 | } |
199 | |
200 | /** |
201 | * Get all the configuration details related to the current theme. |
202 | * |
203 | * @return array |
204 | */ |
205 | public function getThemeInfo() |
206 | { |
207 | // Fill in the theme info cache if it is not already populated: |
208 | if ($this->allThemeInfo === null) { |
209 | // Build an array of theme information by inheriting up the theme tree: |
210 | $this->allThemeInfo = []; |
211 | $currentTheme = $this->getTheme(); |
212 | do { |
213 | $this->loadThemeConfig($currentTheme); |
214 | $currentTheme = $this->allThemeInfo[$currentTheme]['extends']; |
215 | } while ($currentTheme); |
216 | } |
217 | |
218 | return $this->allThemeInfo; |
219 | } |
220 | |
221 | /** |
222 | * Get a configuration element, merged to reflect theme inheritance. |
223 | * |
224 | * @param string $key Configuration key to retrieve (or empty string to |
225 | * retrieve full configuration) |
226 | * |
227 | * @return array|string |
228 | */ |
229 | public function getMergedConfig(string $key = '') |
230 | { |
231 | $currentTheme = $this->getTheme(); |
232 | $allThemeInfo = $this->getThemeInfo(); |
233 | |
234 | $cacheKey = $currentTheme . '_' . $key; |
235 | |
236 | if ($this->cache !== null) { |
237 | $cached = $this->cache->getItem($cacheKey); |
238 | |
239 | if ($cached !== null) { |
240 | return $cached; |
241 | } |
242 | } |
243 | |
244 | $merged = []; |
245 | |
246 | while (!empty($currentTheme)) { |
247 | $currentThemeSet = array_merge( |
248 | (array)$currentTheme, |
249 | $allThemeInfo[$currentTheme]['mixins'] ?? [], |
250 | ); |
251 | |
252 | // from child to parent |
253 | foreach ($currentThemeSet as $theme) { |
254 | if ( |
255 | isset($allThemeInfo[$theme]) |
256 | && (empty($key) || isset($allThemeInfo[$theme][$key])) |
257 | ) { |
258 | $current = empty($key) |
259 | ? $allThemeInfo[$theme] |
260 | : $allThemeInfo[$theme][$key]; |
261 | |
262 | $merged = $this->mergeRecursive($current, $merged); |
263 | } |
264 | } |
265 | |
266 | $currentTheme = $allThemeInfo[$currentTheme]['extends']; |
267 | } |
268 | |
269 | if ($this->cache !== null) { |
270 | $this->cache->setItem($cacheKey, $merged); |
271 | } |
272 | |
273 | return $merged; |
274 | } |
275 | |
276 | /** |
277 | * Search the themes for a particular file. If it exists, return the |
278 | * first matching theme name; otherwise, return null. |
279 | * |
280 | * @param string|array $relativePath Relative path (or array of paths) to |
281 | * search within themes |
282 | * @param string|bool $returnType If boolean true, return full file path; |
283 | * if boolean false, return containing theme name; if self::RETURN_ALL_DETAILS, |
284 | * return an array containing both values (keyed with 'path', 'theme' and |
285 | * 'relativePath'). |
286 | * |
287 | * @return string|array|null |
288 | */ |
289 | public function findContainingTheme($relativePath, $returnType = false) |
290 | { |
291 | $basePath = $this->getBaseDir(); |
292 | $allPaths = is_array($relativePath) |
293 | ? $relativePath : [$relativePath]; |
294 | |
295 | $currentTheme = $this->getTheme(); |
296 | $allThemeInfo = $this->getThemeInfo(); |
297 | |
298 | while (!empty($currentTheme)) { |
299 | $currentThemeSet = array_merge( |
300 | (array)$currentTheme, |
301 | $allThemeInfo[$currentTheme]['mixins'] ?? [] |
302 | ); |
303 | foreach ($currentThemeSet as $theme) { |
304 | foreach ($allPaths as $currentPath) { |
305 | $path = "$basePath/$theme/$currentPath"; |
306 | if (file_exists($path)) { |
307 | // Depending on return type, send back the requested data: |
308 | if (self::RETURN_ALL_DETAILS === $returnType) { |
309 | $relativePath = $currentPath; |
310 | return compact('path', 'theme', 'relativePath'); |
311 | } |
312 | return $returnType ? $path : $theme; |
313 | } |
314 | } |
315 | } |
316 | $currentTheme = $allThemeInfo[$currentTheme]['extends']; |
317 | } |
318 | |
319 | return null; |
320 | } |
321 | |
322 | /** |
323 | * Search the themes for a file pattern. Returns all matching files. |
324 | * |
325 | * Note that for any matching file the last match in the theme hierarchy is |
326 | * returned. |
327 | * |
328 | * @param string|array $relativePathPattern Relative path pattern (or array of |
329 | * patterns) to search within themes |
330 | * |
331 | * @return array |
332 | */ |
333 | public function findInThemes($relativePathPattern) |
334 | { |
335 | $basePath = $this->getBaseDir(); |
336 | $allPaths = (array)$relativePathPattern; |
337 | |
338 | $currentTheme = $this->getTheme(); |
339 | $allThemeInfo = $this->getThemeInfo(); |
340 | |
341 | $allThemes = []; |
342 | while (!empty($currentTheme)) { |
343 | $allThemes = array_merge( |
344 | $allThemes, |
345 | (array)$currentTheme, |
346 | $allThemeInfo[$currentTheme]['mixins'] ?? [] |
347 | ); |
348 | $currentTheme = $allThemeInfo[$currentTheme]['extends']; |
349 | } |
350 | |
351 | // Start from the base theme so that we can find any overrides properly |
352 | $allThemes = array_reverse($allThemes); |
353 | $results = []; |
354 | foreach ($allThemes as $theme) { |
355 | $themePath = "$basePath/$theme/"; |
356 | foreach ($allPaths as $currentPath) { |
357 | $path = $themePath . $currentPath; |
358 | foreach (Glob::glob($path) as $file) { |
359 | if (filetype($file) === 'dir') { |
360 | continue; |
361 | } |
362 | $relativeFile = substr($file, strlen($themePath)); |
363 | $results[$relativeFile] = [ |
364 | 'theme' => $theme, |
365 | 'file' => $file, |
366 | 'relativeFile' => $relativeFile, |
367 | ]; |
368 | } |
369 | } |
370 | } |
371 | return array_values($results); |
372 | } |
373 | } |