Passed
Push — 4 ( 20c805...de0b76 )
by Damian
08:20
created

ThemeResourceLoader::getThemePaths()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

304
            $relativePath = Path::join($themePath, /** @scrutinizer ignore-type */ $resource);
Loading history...
305
            $absolutePath = Path::join($this->base, $relativePath);
306
            if (file_exists($absolutePath)) {
307
                return $relativePath;
308
            }
309
        }
310
311
        // Resource exists in no context
312
        return null;
313
    }
314
315
    /**
316
     * Resolve all themes to the list of root folders relative to site root
317
     *
318
     * @param array $themes List of themes to resolve. Supports named theme sets. Defaults to {@see SSViewer::get_themes()}.
319
     * @return array List of root-relative folders in order of precendence.
320
     */
321
    public function getThemePaths($themes = null)
322
    {
323
        if ($themes === null) {
324
            $themes = SSViewer::get_themes();
325
        }
326
327
        $paths = [];
328
        foreach ($themes as $themename) {
329
            // Expand theme sets
330
            $set = $this->getSet($themename);
331
            $subthemes = $set ? $set->getThemes() : [$themename];
332
333
            // Resolve paths
334
            foreach ($subthemes as $theme) {
335
                $paths[] = $this->getPath($theme);
336
            }
337
        }
338
        return $paths;
339
    }
340
}
341