Passed
Push — master ( 102f9d...99342a )
by Robbie
09:22
created

ThemeResourceLoader::setCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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