Completed
Push — resourceloader ( fc8821...2b8874 )
by Sam
09:07
created

ResourceLoader   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 288
rs 8.2857
c 0
b 0
f 0
wmc 39
lcom 2
cbo 5

11 Methods

Rating   Name   Duplication   Size   Complexity  
A inst() 0 4 2
A set_instance() 0 4 1
A __construct() 0 4 2
A addSet() 0 4 1
A getSet() 0 7 2
A getResourcePath() 0 7 2
A getResourceURL() 0 7 2
B getRelativeResourcePath() 0 18 5
B getThemePath() 0 54 7
C findTemplate() 0 53 11
A getThemePaths() 0 15 4
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
// TO DO: allow pluggable provider of client URLs and move that plugin to SilverStripe\Control.
6
use SilverStripe\Control\Director;
7
8
/**
9
 * Finds resources from 1 or more themes/modules.
10
 */
11
class ResourceLoader
12
{
13
14
    /**
15
     * @var ResourceLoader
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 ResourceLoader
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 ResourceLoader $instance
41
     */
42
    public static function set_instance(ResourceLoader $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
     * Returns the absolute path to the given resource.
79
     *
80
     * @param string|array $module 1 or more modules or themes to searrh
81
     * @return string|null The absolute path to the file, if it exists. Returns null if the file can't be found
82
     */
83
    public function getResourcePath($module, $resource)
84
    {
85
        $path = $this->getRelativeResourcePath($module, $resource);
86
        if ($path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
87
            return $this->base . '/' . $path;
88
        }
89
    }
90
91
    /**
92
     * Returns the URL of the given resource.
93
     *
94
     * The URL will be relative to the domain root, unless assets are on a different path (not currently supported but
95
     * may be added in the future.
96
     *
97
     * @param string|array $module A module or list of modules to
98
     * @return string|null The URL of the resource, if it exists
99
     */
100
    public function getResourceURL($module, $resource)
101
    {
102
        $path = $this->getRelativeResourcePath($module, $resource);
103
        if ($path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
104
            return Director::baseURL() . $path;
105
        }
106
    }
107
108
    /**
109
     * Returns the path to the given resource, relative to BASE_PATH
110
     *
111
     * @param string|array $module 1 or more modules or themes to searrh
112
     * @return string|null The absolute path to the file, if it exists. Returns null if the file can't be found
113
     */
114
    public function getRelativeResourcePath($module, $resource)
115
    {
116
        if ($resource[0] !== '/') {
117
            $resource = '/' . $resource;
118
        }
119
120
        $paths = $this->getThemePaths(is_array($module) ? $module : [$module]);
121
122
        foreach ($paths as $themePath) {
123
            $abspath = $themePath;
124
            if (file_exists(BASE_PATH . '/' . $abspath . $resource)) {
125
                return $themePath . $resource;
126
            }
127
        }
128
129
        // Resource exists in no context
130
        return null;
131
    }
132
133
    /**
134
     * Given a theme identifier, determine the path from the root directory
135
     *
136
     * The mapping from $identifier to path follows these rules:
137
     * - A simple theme name ('mytheme') which maps to the standard themes dir (/themes/mytheme)
138
     * - A theme path with a leading slash ('/mymodule/themes/mytheme') which maps directly to that path.
139
     * - or a vendored theme path. (vendor/mymodule:mytheme) which maps to the nested 'theme' within
140
     *   that module. ('/mymodule/themes/mytheme').
141
     * - A vendored module with no nested theme (vendor/mymodule) which maps to the root directory
142
     *   of that module. ('/mymodule').
143
     *
144
     * @param string $identifier Theme identifier.
145
     * @return string Path from root, not including leading or trailing forward slash. E.g. themes/mytheme
146
     */
147
    public function getThemePath($identifier)
148
    {
149
        $slashPos = strpos($identifier, '/');
150
151
        // If identifier starts with "/", it's a path from root
152
        if ($slashPos === 0) {
153
            return substr($identifier, 1);
154
        } // Otherwise if there is a "/", identifier is a vendor'ed module
155
        elseif ($slashPos !== false) {
156
            // Extract from <vendor>/<module>:<theme> format.
157
            // <vendor> is optional, and if <theme> is omitted it defaults to the module root dir.
158
            // If <theme> is included, this is the name of the directory under moduleroot/themes/
159
            // which contains the theme.
160
            // <module> is always the name of the install directory, not necessarily the composer name.
161
            $parts = explode(':', $identifier, 2);
162
163
            if (count($parts) > 1) {
164
                $theme = $parts[1];
165
                // "module/vendor:/sub/path"
166
                if ($theme[0] === '/') {
167
                    $subpath = $theme;
168
169
                // "module/vendor:subtheme"
170
                } else {
171
                    $subpath = '/themes/' . $theme;
172
                }
173
174
            // "module/vendor"
175
            } else {
176
                $subpath = '';
177
            }
178
179
            $package = $parts[0];
180
181
            // Find matching module for this package
182
            $module = ModuleLoader::inst()->getManifest()->getModule($package);
183
            if ($module) {
184
                $modulePath = $module->getRelativePath();
185
            } else {
186
                // fall back to dirname
187
                list(, $modulePath) = explode('/', $parts[0], 2);
188
189
                // If the module is in the themes/<module>/ prefer that
190
                if (is_dir(THEMES_PATH . '/' .$modulePath)) {
191
                    $modulePath = THEMES_DIR . '/' . $$modulePath;
192
                }
193
            }
194
195
            return ltrim($modulePath . $subpath, '/');
196
        } // Otherwise it's a (deprecated) old-style "theme" identifier
197
        else {
198
            return THEMES_DIR.'/'.$identifier;
199
        }
200
    }
201
202
    /**
203
     * Attempts to find possible candidate templates from a set of template
204
     * names from modules, current theme directory and finally the application
205
     * folder.
206
     *
207
     * The template names can be passed in as plain strings, or be in the
208
     * format "type/name", where type is the type of template to search for
209
     * (e.g. Includes, Layout).
210
     *
211
     * @param string|array $template Template name, or template spec in array format with the keys
212
     * 'type' (type string) and 'templates' (template hierarchy in order of precedence).
213
     * If 'templates' is ommitted then any other item in the array will be treated as the template
214
     * list, or list of templates each in the array spec given.
215
     * Templates with an .ss extension will be treated as file paths, and will bypass
216
     * theme-coupled resolution.
217
     * @param array $themes List of themes to use to resolve themes. In most cases
218
     * you should pass in {@see SSViewer::get_themes()}
219
     * @return string Absolute path to resolved template file, or null if not resolved.
220
     * File location will be in the format themes/<theme>/templates/<directories>/<type>/<basename>.ss
221
     * Note that type (e.g. 'Layout') is not the root level directory under 'templates'.
222
     */
223
    public function findTemplate($template, $themes)
224
    {
225
        $type = '';
226
        if (is_array($template)) {
227
            // Check if templates has type specified
228
            if (array_key_exists('type', $template)) {
229
                $type = $template['type'];
230
                unset($template['type']);
231
            }
232
            // Templates are either nested in 'templates' or just the rest of the list
233
            $templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
234
        } else {
235
            $templateList = array($template);
236
        }
237
238
        foreach ($templateList as $i => $template) {
239
            // Check if passed list of templates in array format
240
            if (is_array($template)) {
241
                $path = $this->findTemplate($template, $themes);
242
                if ($path) {
243
                    return $path;
244
                }
245
                continue;
246
            }
247
248
            // If we have an .ss extension, this is a path, not a template name. We should
249
            // pass in templates without extensions in order for template manifest to find
250
            // files dynamically.
251
            if (substr($template, -3) == '.ss' && file_exists($template)) {
252
                return $template;
253
            }
254
255
            // Check string template identifier
256
            $template = str_replace('\\', '/', $template);
257
            $parts = explode('/', $template);
258
259
            $tail = array_pop($parts);
260
            $head = implode('/', $parts);
261
262
            $themePaths = $this->getThemePaths($themes);
263
            foreach ($themePaths as $themePath) {
264
                // Join path
265
                $pathParts = [ $this->base, $themePath, 'templates', $head, $type, $tail ];
266
                $path = implode('/', array_filter($pathParts)) . '.ss';
267
                if (file_exists($path)) {
268
                    return $path;
269
                }
270
            }
271
        }
272
273
        // No template found
274
        return null;
275
    }
276
277
    /**
278
     * Resolve all themes to the list of root folders relative to site root
279
     *
280
     * @param array $themes List of themes to resolve. Supports named theme sets.
281
     * @return array List of root-relative folders in order of precendence.
282
     */
283
    public function getThemePaths($themes)
284
    {
285
        $paths = [];
286
        foreach ($themes as $themename) {
287
            // Expand theme sets
288
            $set = $this->getSet($themename);
289
            $subthemes = $set ? $set->getThemes() : [$themename];
290
291
            // Resolve paths
292
            foreach ($subthemes as $theme) {
293
                $paths[] = $this->getThemePath($theme);
294
            }
295
        }
296
        return $paths;
297
    }
298
}
299