Completed
Push — master ( 80bff0...2aa3b5 )
by Damian
08:40
created

ThemeResourceLoader::inst()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View;
4
5
use SilverStripe\Core\Manifest\ModuleLoader;
6
use SilverStripe\Dev\Debug;
7
8
/**
9
 * Handles finding templates from a stack of template manifest objects.
10
 */
11
class ThemeResourceLoader
12
{
13
14
    /**
15
     * @var ThemeResourceLoader
16
     */
17
    private static $instance;
18
19
    protected $base;
20
21
    /**
22
     * List of template "sets" that contain a test manifest, and have an alias.
23
     * E.g. '$default'
24
     *
25
     * @var ThemeList[]
26
     */
27
    protected $sets = [];
28
29
    /**
30
     * @return ThemeResourceLoader
31
     */
32
    public static function inst()
33
    {
34
        return self::$instance ? self::$instance : self::$instance = new self();
35
    }
36
37
    /**
38
     * Set instance
39
     *
40
     * @param ThemeResourceLoader $instance
41
     */
42
    public static function set_instance(ThemeResourceLoader $instance)
43
    {
44
        self::$instance = $instance;
45
    }
46
47
    public function __construct($base = null)
48
    {
49
        $this->base = $base ? $base : BASE_PATH;
50
    }
51
52
    /**
53
     * Add a new theme manifest for a given identifier. E.g. '$default'
54
     *
55
     * @param string $set
56
     * @param ThemeList $manifest
57
     */
58
    public function addSet($set, ThemeList $manifest)
59
    {
60
        $this->sets[$set] = $manifest;
61
    }
62
63
    /**
64
     * Get a named theme set
65
     *
66
     * @param string $set
67
     * @return ThemeList
68
     */
69
    public function getSet($set)
70
    {
71
        if (isset($this->sets[$set])) {
72
            return $this->sets[$set];
73
        }
74
        return null;
75
    }
76
77
    /**
78
     * Given a theme identifier, determine the path from the root directory
79
     *
80
     * The mapping from $identifier to path follows these rules:
81
     * - A simple theme name ('mytheme') which maps to the standard themes dir (/themes/mytheme)
82
     * - A theme path with a leading slash ('/mymodule/themes/mytheme') which maps directly to that path.
83
     * - or a vendored theme path. (vendor/mymodule:mytheme) which maps to the nested 'theme' within
84
     *   that module. ('/mymodule/themes/mytheme').
85
     * - A vendored module with no nested theme (vendor/mymodule) which maps to the root directory
86
     *   of that module. ('/mymodule').
87
     *
88
     * @param string $identifier Theme identifier.
89
     * @return string Path from root, not including leading or trailing forward slash. E.g. themes/mytheme
90
     */
91
    public function getPath($identifier)
92
    {
93
        $slashPos = strpos($identifier, '/');
94
95
        // If identifier starts with "/", it's a path from root
96
        if ($slashPos === 0) {
97
            return substr($identifier, 1);
98
        } // Otherwise if there is a "/", identifier is a vendor'ed module
99
        elseif ($slashPos !== false) {
100
            // Extract from <vendor>/<module>:<theme> format.
101
            // <vendor> is optional, and if <theme> is omitted it defaults to the module root dir.
102
            // If <theme> is included, this is the name of the directory under moduleroot/themes/
103
            // which contains the theme.
104
            // <module> is always the name of the install directory, not necessarily the composer name.
105
            $parts = explode(':', $identifier, 2);
106
107
            if (count($parts) > 1) {
108
                $theme = $parts[1];
109
                // "module/vendor:/sub/path"
110
                if ($theme[0] === '/') {
111
                    $subpath = $theme;
112
113
                // "module/vendor:subtheme"
114
                } else {
115
                    $subpath = '/themes/' . $theme;
116
                }
117
118
            // "module/vendor"
119
            } else {
120
                $subpath = '';
121
            }
122
123
            $package = $parts[0];
124
125
            // Find matching module for this package
126
            $module = ModuleLoader::inst()->getManifest()->getModule($package);
127
            if ($module) {
128
                $modulePath = $module->getRelativePath();
129
            } else {
130
                // fall back to dirname
131
                list(, $modulePath) = explode('/', $parts[0], 2);
132
133
                // If the module is in the themes/<module>/ prefer that
134
                if (is_dir(THEMES_PATH . '/' .$modulePath)) {
135
                    $modulePath = THEMES_DIR . '/' . $$modulePath;
136
                }
137
            }
138
139
            return ltrim($modulePath . $subpath, '/');
140
        } // Otherwise it's a (deprecated) old-style "theme" identifier
141
        else {
142
            return THEMES_DIR.'/'.$identifier;
143
        }
144
    }
145
146
    /**
147
     * Attempts to find possible candidate templates from a set of template
148
     * names from modules, current theme directory and finally the application
149
     * folder.
150
     *
151
     * The template names can be passed in as plain strings, or be in the
152
     * format "type/name", where type is the type of template to search for
153
     * (e.g. Includes, Layout).
154
     *
155
     * @param string|array $template Template name, or template spec in array format with the keys
156
     * 'type' (type string) and 'templates' (template hierarchy in order of precedence).
157
     * If 'templates' is ommitted then any other item in the array will be treated as the template
158
     * list, or list of templates each in the array spec given.
159
     * Templates with an .ss extension will be treated as file paths, and will bypass
160
     * theme-coupled resolution.
161
     * @param array $themes List of themes to use to resolve themes. In most cases
162
     * you should pass in {@see SSViewer::get_themes()}
163
     * @return string Absolute path to resolved template file, or null if not resolved.
164
     * File location will be in the format themes/<theme>/templates/<directories>/<type>/<basename>.ss
165
     * Note that type (e.g. 'Layout') is not the root level directory under 'templates'.
166
     */
167
    public function findTemplate($template, $themes)
168
    {
169
        $type = '';
170
        if (is_array($template)) {
171
            // Check if templates has type specified
172
            if (array_key_exists('type', $template)) {
173
                $type = $template['type'];
174
                unset($template['type']);
175
            }
176
            // Templates are either nested in 'templates' or just the rest of the list
177
            $templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
178
        } else {
179
            $templateList = array($template);
180
        }
181
182
        foreach ($templateList as $i => $template) {
183
            // Check if passed list of templates in array format
184
            if (is_array($template)) {
185
                $path = $this->findTemplate($template, $themes);
186
                if ($path) {
187
                    return $path;
188
                }
189
                continue;
190
            }
191
192
            // If we have an .ss extension, this is a path, not a template name. We should
193
            // pass in templates without extensions in order for template manifest to find
194
            // files dynamically.
195
            if (substr($template, -3) == '.ss' && file_exists($template)) {
196
                return $template;
197
            }
198
199
            // Check string template identifier
200
            $template = str_replace('\\', '/', $template);
201
            $parts = explode('/', $template);
202
203
            $tail = array_pop($parts);
204
            $head = implode('/', $parts);
205
206
            $themePaths = $this->getThemePaths($themes);
207
            foreach ($themePaths as $themePath) {
208
                // Join path
209
                $pathParts = [ $this->base, $themePath, 'templates', $head, $type, $tail ];
210
                $path = implode('/', array_filter($pathParts)) . '.ss';
211
                if (file_exists($path)) {
212
                    return $path;
213
                }
214
            }
215
        }
216
217
        // No template found
218
        return null;
219
    }
220
221
    /**
222
     * Resolve themed CSS path
223
     *
224
     * @param string $name Name of CSS file without extension
225
     * @param array $themes List of themes
226
     * @return string Path to resolved CSS file (relative to base dir)
227
     */
228
    public function findThemedCSS($name, $themes)
229
    {
230
        if (substr($name, -4) !== '.css') {
231
            $name .= '.css';
232
        }
233
234
        $filename = $this->findThemedResource("css/$name", $themes);
235
        if ($filename === null) {
236
            $filename = $this->findThemedResource($name, $themes);
237
        }
238
239
        return $filename;
240
    }
241
242
    /**
243
     * Resolve themed javascript path
244
     *
245
     * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
246
     * and it that doesn't exist and the module parameter is set then a javascript file with that name in
247
     * the module is used.
248
     *
249
     * @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
250
     * @param array $themes List of themes
251
     * @return string Path to resolved javascript file (relative to base dir)
252
     */
253
    public function findThemedJavascript($name, $themes)
254
    {
255
        if (substr($name, -3) !== '.js') {
256
            $name .= '.js';
257
        }
258
259
        $filename = $this->findThemedResource("javascript/$name", $themes);
260
        if ($filename === null) {
261
            $filename = $this->findThemedResource($name, $themes);
262
        }
263
264
        return $filename;
265
    }
266
267
    /**
268
     * Resolve a themed resource
269
     *
270
     * A themed resource and be any file that resides in a theme folder.
271
     *
272
     * @param string $resource A file path relative to the root folder of a theme
273
     * @param array $themes An order listed of themes to search
274
     */
275
    public function findThemedResource($resource, $themes)
276
    {
277
        if ($resource[0] !== '/') {
278
            $resource = '/' . $resource;
279
        }
280
281
        $paths = $this->getThemePaths($themes);
282
283
        foreach ($paths as $themePath) {
284
            $abspath = $this->base . '/' . $themePath;
285
            if (file_exists($abspath . $resource)) {
286
                return $themePath . $resource;
287
            }
288
        }
289
290
        // Resource exists in no context
291
        return null;
292
    }
293
294
    /**
295
     * Resolve all themes to the list of root folders relative to site root
296
     *
297
     * @param array $themes List of themes to resolve. Supports named theme sets.
298
     * @return array List of root-relative folders in order of precendence.
299
     */
300
    public function getThemePaths($themes)
301
    {
302
        $paths = [];
303
        foreach ($themes as $themename) {
304
            // Expand theme sets
305
            $set = $this->getSet($themename);
306
            $subthemes = $set ? $set->getThemes() : [$themename];
307
308
            // Resolve paths
309
            foreach ($subthemes as $theme) {
310
                $paths[] = $this->getPath($theme);
311
            }
312
        }
313
        return $paths;
314
    }
315
}
316