Passed
Push — 4 ( 766816...36b106 )
by Maxime
06:12
created

ThemeResourceLoader::getCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View;
4
5
use InvalidArgumentException;
6
use Psr\SimpleCache\CacheInterface;
7
use SilverStripe\Core\Flushable;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Core\Manifest\ModuleLoader;
10
use SilverStripe\Core\Path;
11
12
/**
13
 * Handles finding templates from a stack of template manifest objects.
14
 */
15
class ThemeResourceLoader implements Flushable
16
{
17
18
    /**
19
     * @var ThemeResourceLoader
20
     */
21
    private static $instance;
22
23
    /**
24
     * Internal memory cache for large sets of repeated calls
25
     *
26
     * @var array
27
     */
28
    protected static $cacheData = [];
29
30
    /**
31
     * The base path of the application
32
     *
33
     * @var string
34
     */
35
    protected $base;
36
37
    /**
38
     * List of template "sets" that contain a test manifest, and have an alias.
39
     * E.g. '$default'
40
     *
41
     * @var ThemeList[]
42
     */
43
    protected $sets = [];
44
45
    /**
46
     * @var CacheInterface
47
     */
48
    protected $cache;
49
50
    /**
51
     * @return ThemeResourceLoader
52
     */
53
    public static function inst()
54
    {
55
        return self::$instance ? self::$instance : self::$instance = new self();
56
    }
57
58
    /**
59
     * Set instance
60
     *
61
     * @param ThemeResourceLoader $instance
62
     */
63
    public static function set_instance(ThemeResourceLoader $instance)
64
    {
65
        self::$instance = $instance;
66
    }
67
68
    public function __construct($base = null)
69
    {
70
        $this->base = $base ? $base : BASE_PATH;
71
    }
72
73
    /**
74
     * Add a new theme manifest for a given identifier. E.g. '$default'
75
     *
76
     * @param string $set
77
     * @param ThemeList $manifest
78
     */
79
    public function addSet($set, ThemeList $manifest)
80
    {
81
        $this->sets[$set] = $manifest;
82
    }
83
84
    /**
85
     * Get a named theme set
86
     *
87
     * @param string $set
88
     * @return ThemeList
89
     */
90
    public function getSet($set)
91
    {
92
        if (isset($this->sets[$set])) {
93
            return $this->sets[$set];
94
        }
95
        return null;
96
    }
97
98
    /**
99
     * Given a theme identifier, determine the path from the root directory
100
     *
101
     * The mapping from $identifier to path follows these rules:
102
     * - A simple theme name ('mytheme') which maps to the standard themes dir (/themes/mytheme)
103
     * - A theme path with a leading slash ('/mymodule/themes/mytheme') which maps directly to that path.
104
     * - or a vendored theme path. (vendor/mymodule:mytheme) which maps to the nested 'theme' within
105
     *   that module. ('/mymodule/themes/mytheme').
106
     * - A vendored module with no nested theme (vendor/mymodule) which maps to the root directory
107
     *   of that module. ('/mymodule').
108
     *
109
     * @param string $identifier Theme identifier.
110
     * @return string Path from root, not including leading or trailing forward slash. E.g. themes/mytheme
111
     */
112
    public function getPath($identifier)
113
    {
114
        $slashPos = strpos($identifier, '/');
115
        $parts = explode(':', $identifier, 2);
116
117
        // If identifier starts with "/", it's a path from root
118
        if ($slashPos === 0) {
119
            if (count($parts) > 1) {
120
                throw new InvalidArgumentException("Invalid theme identifier {$identifier}");
121
            }
122
            return Path::normalise($identifier, true);
123
        }
124
125
        // If there is no slash / colon it's a legacy theme
126
        if ($slashPos === false && count($parts) === 1) {
0 ignored issues
show
introduced by
The condition $slashPos === false is always false.
Loading history...
127
            return Path::join(THEMES_DIR, $identifier);
128
        }
129
130
        // Extract from <vendor>/<module>:<theme> format.
131
        // <vendor> is optional, and if <theme> is omitted it defaults to the module root dir.
132
        // If <theme> is included, this is the name of the directory under moduleroot/themes/
133
        // which contains the theme.
134
        // <module> is always the name of the install directory, not necessarily the composer name.
135
136
        // Find module from first part
137
        $moduleName = $parts[0];
138
        $module = ModuleLoader::inst()->getManifest()->getModule($moduleName);
139
        if ($module) {
0 ignored issues
show
introduced by
$module is of type SilverStripe\Core\Manifest\Module, thus it always evaluated to true.
Loading history...
140
            $modulePath = $module->getRelativePath();
141
        } else {
142
            // If no module could be found, assume based on basename
143
            // with a warning
144
            if (strstr('/', $moduleName)) {
145
                list(, $modulePath) = explode('/', $parts[0], 2);
146
            } else {
147
                $modulePath = $moduleName;
148
            }
149
            trigger_error("No module named {$moduleName} found. Assuming path {$modulePath}", E_USER_WARNING);
150
        }
151
152
        // Parse relative path for this theme within this module
153
        $theme = count($parts) > 1 ? $parts[1] : '';
154
        if (empty($theme)) {
155
            // "module/vendor:"
156
            // "module/vendor"
157
            $subpath = '';
158
        } elseif (strpos($theme, '/') === 0) {
159
            // "module/vendor:/sub/path"
160
            $subpath = rtrim($theme, '/');
161
        } else {
162
            // "module/vendor:subtheme"
163
            $subpath = '/themes/' . $theme;
164
        }
165
166
        // Join module with subpath
167
        return Path::normalise($modulePath . $subpath, true);
168
    }
169
170
    /**
171
     * Attempts to find possible candidate templates from a set of template
172
     * names from modules, current theme directory and finally the application
173
     * folder.
174
     *
175
     * The template names can be passed in as plain strings, or be in the
176
     * format "type/name", where type is the type of template to search for
177
     * (e.g. Includes, Layout).
178
     *
179
     * The results of this method will be cached for future use.
180
     *
181
     * @param string|array $template Template name, or template spec in array format with the keys
182
     * 'type' (type string) and 'templates' (template hierarchy in order of precedence).
183
     * If 'templates' is ommitted then any other item in the array will be treated as the template
184
     * list, or list of templates each in the array spec given.
185
     * Templates with an .ss extension will be treated as file paths, and will bypass
186
     * theme-coupled resolution.
187
     * @param array $themes List of themes to use to resolve themes. Defaults to {@see SSViewer::get_themes()}
188
     * @return string Absolute path to resolved template file, or null if not resolved.
189
     * File location will be in the format themes/<theme>/templates/<directories>/<type>/<basename>.ss
190
     * Note that type (e.g. 'Layout') is not the root level directory under 'templates'.
191
     */
192
    public function findTemplate($template, $themes = null)
193
    {
194
        if ($themes === null) {
195
            $themes = SSViewer::get_themes();
196
        }
197
198
        // Look for a cached result for this data set
199
        $cacheKey = md5(json_encode($template) . json_encode($themes));
200
        if ($this->getCache()->has($cacheKey)) {
201
            return $this->getCache()->get($cacheKey);
202
        }
203
204
        $type = '';
205
        if (is_array($template)) {
206
            // Check if templates has type specified
207
            if (array_key_exists('type', $template)) {
208
                $type = $template['type'];
209
                unset($template['type']);
210
            }
211
            // Templates are either nested in 'templates' or just the rest of the list
212
            $templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
213
        } else {
214
            $templateList = array($template);
215
        }
216
217
        foreach ($templateList as $i => $template) {
0 ignored issues
show
introduced by
$template is overwriting one of the parameters of this function.
Loading history...
218
            // Check if passed list of templates in array format
219
            if (is_array($template)) {
220
                $path = $this->findTemplate($template, $themes);
221
                if ($path) {
222
                    $this->getCache()->set($cacheKey, $path);
223
                    return $path;
224
                }
225
                continue;
226
            }
227
228
            // If we have an .ss extension, this is a path, not a template name. We should
229
            // pass in templates without extensions in order for template manifest to find
230
            // files dynamically.
231
            if (substr($template, -3) == '.ss' && file_exists($template)) {
232
                $this->getCache()->set($cacheKey, $template);
233
                return $template;
234
            }
235
236
            // Check string template identifier
237
            $template = str_replace('\\', '/', $template);
238
            $parts = explode('/', $template);
239
240
            $tail = array_pop($parts);
241
            $head = implode('/', $parts);
242
            $themePaths = $this->getThemePaths($themes);
243
            foreach ($themePaths as $themePath) {
244
                // Join path
245
                $pathParts = [ $this->base, $themePath, 'templates', $head, $type, $tail ];
246
                $path = Path::join($pathParts) . '.ss';
247
                if (file_exists($path)) {
248
                    $this->getCache()->set($cacheKey, $path);
249
                    return $path;
250
                }
251
            }
252
        }
253
254
        // No template found
255
        $this->getCache()->set($cacheKey, null);
256
        return null;
257
    }
258
259
    /**
260
     * Resolve themed CSS path
261
     *
262
     * @param string $name Name of CSS file without extension
263
     * @param array $themes List of themes, Defaults to {@see SSViewer::get_themes()}
264
     * @return string Path to resolved CSS file (relative to base dir)
265
     */
266
    public function findThemedCSS($name, $themes = null)
267
    {
268
        if ($themes === null) {
269
            $themes = SSViewer::get_themes();
270
        }
271
272
        if (substr($name, -4) !== '.css') {
273
            $name .= '.css';
274
        }
275
276
        $filename = $this->findThemedResource("css/$name", $themes);
277
        if ($filename === null) {
0 ignored issues
show
introduced by
The condition $filename === null is always false.
Loading history...
278
            $filename = $this->findThemedResource($name, $themes);
279
        }
280
281
        return $filename;
282
    }
283
284
    /**
285
     * Resolve themed javascript path
286
     *
287
     * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
288
     * and it that doesn't exist and the module parameter is set then a javascript file with that name in
289
     * the module is used.
290
     *
291
     * @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
292
     * @param array $themes List of themes, Defaults to {@see SSViewer::get_themes()}
293
     * @return string Path to resolved javascript file (relative to base dir)
294
     */
295
    public function findThemedJavascript($name, $themes = null)
296
    {
297
        if ($themes === null) {
298
            $themes = SSViewer::get_themes();
299
        }
300
301
        if (substr($name, -3) !== '.js') {
302
            $name .= '.js';
303
        }
304
305
        $filename = $this->findThemedResource("javascript/$name", $themes);
306
        if ($filename === null) {
0 ignored issues
show
introduced by
The condition $filename === null is always false.
Loading history...
307
            $filename = $this->findThemedResource($name, $themes);
308
        }
309
310
        return $filename;
311
    }
312
313
    /**
314
     * Resolve a themed resource
315
     *
316
     * A themed resource and be any file that resides in a theme folder.
317
     *
318
     * @param string $resource A file path relative to the root folder of a theme
319
     * @param array $themes An order listed of themes to search, Defaults to {@see SSViewer::get_themes()}
320
     * @return string
321
     */
322
    public function findThemedResource($resource, $themes = null)
323
    {
324
        if ($themes === null) {
325
            $themes = SSViewer::get_themes();
326
        }
327
328
        $paths = $this->getThemePaths($themes);
329
330
        foreach ($paths as $themePath) {
331
            $relativePath = Path::join($themePath, $resource);
332
            $absolutePath = Path::join($this->base, $relativePath);
333
            if (file_exists($absolutePath)) {
334
                return $relativePath;
335
            }
336
        }
337
338
        // Resource exists in no context
339
        return null;
340
    }
341
342
    /**
343
     * Resolve all themes to the list of root folders relative to site root
344
     *
345
     * @param array $themes List of themes to resolve. Supports named theme sets. Defaults to {@see SSViewer::get_themes()}.
346
     * @return array List of root-relative folders in order of precendence.
347
     */
348
    public function getThemePaths($themes = null)
349
    {
350
        if ($themes === null) {
351
            $themes = SSViewer::get_themes();
352
        }
353
354
        $paths = [];
355
        foreach ($themes as $themename) {
356
            // Expand theme sets
357
            $set = $this->getSet($themename);
358
            $subthemes = $set ? $set->getThemes() : [$themename];
359
360
            // Resolve paths
361
            foreach ($subthemes as $theme) {
362
                $paths[] = $this->getPath($theme);
363
            }
364
        }
365
        return $paths;
366
    }
367
368
    /**
369
     * Flush any cached data
370
     */
371
    public static function flush()
372
    {
373
        self::inst()->getCache()->clear();
374
    }
375
376
    /**
377
     * @return CacheInterface
378
     */
379
    public function getCache()
380
    {
381
        if (!$this->cache) {
382
            $this->setCache(Injector::inst()->get(CacheInterface::class . '.ThemeResourceLoader'));
383
        }
384
        return $this->cache;
385
    }
386
387
    /**
388
     * @param CacheInterface $cache
389
     * @return ThemeResourceLoader
390
     */
391
    public function setCache(CacheInterface $cache)
392
    {
393
        $this->cache = $cache;
394
        return $this;
395
    }
396
}
397