Completed
Push — resourceloader ( 44d0c9...e91ace )
by Sam
08:53
created

ResourceLoader::getRelativeResourcePath()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 6
nop 2
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
/**
6
 * Finds resources from 1 or more themes/modules.
7
 */
8
class ResourceLoader
9
{
10
11
    /**
12
     * @var ResourceLoader
13
     */
14
    private static $instance;
15
16
    protected $base;
17
18
    /**
19
     * List of template "sets" that contain a test manifest, and have an alias.
20
     * E.g. '$default'
21
     *
22
     * @var ThemeList[]
23
     */
24
    protected $sets = [];
25
26
    /**
27
     * @return ResourceLoader
28
     */
29
    public static function inst()
30
    {
31
        return self::$instance ? self::$instance : self::$instance = new self();
32
    }
33
34
    /**
35
     * Set instance
36
     *
37
     * @param ResourceLoader $instance
38
     */
39
    public static function set_instance(ResourceLoader $instance)
40
    {
41
        self::$instance = $instance;
42
    }
43
44
    public function __construct($base = null)
45
    {
46
        $this->base = $base ? $base : BASE_PATH;
47
    }
48
49
    /**
50
     * Add a new theme manifest for a given identifier. E.g. '$default'
51
     *
52
     * @param string $set
53
     * @param ThemeList $manifest
54
     */
55
    public function addSet($set, ThemeList $manifest)
56
    {
57
        $this->sets[$set] = $manifest;
58
    }
59
60
    /**
61
     * Get a named theme set
62
     *
63
     * @param string $set
64
     * @return ThemeList
65
     */
66
    public function getSet($set)
67
    {
68
        if (isset($this->sets[$set])) {
69
            return $this->sets[$set];
70
        }
71
        return null;
72
    }
73
74
    /**
75
     * Returns the absolute path to the given resource.
76
     *
77
     * @param string|array $module 1 or more modules or themes to searrh
78
     * @return string|null The absolute path to the file, if it exists. Returns null if the file can't be found
79
     */
80
    public function getResourcePath($module, $resource)
81
    {
82
        $path = $this->getRelativeResourcePath($module, $resource);
83
        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...
84
            return $this->base . '/' . $path;
85
        }
86
    }
87
88
    /**
89
     * Returns the URL of the given resource.
90
     *
91
     * The URL will be relative to the domain root, unless assets are on a different path (not currently supported but
92
     * may be added in the future.
93
     *
94
     * @param string|array $module A module or list of modules to
95
     * @return string|null The URL of the resource, if it exists
96
     */
97
    public function getResourceURL($module, $resource)
98
    {
99
        $path = $this->getRelativeResourcePath($module, $resource);
100
        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...
101
            return Director::baseURL() . $path;
102
        }
103
    }
104
105
    /**
106
     * Returns the path to the given resource, relative to BASE_PATH
107
     *
108
     * @param string|array $module 1 or more modules or themes to searrh
109
     * @return string|null The absolute path to the file, if it exists. Returns null if the file can't be found
110
     */
111
    public function getRelativeResourcePath($module, $resource)
0 ignored issues
show
Unused Code introduced by
The parameter $module is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
112
    {
113
        if ($resource[0] !== '/') {
114
            $resource = '/' . $resource;
115
        }
116
117
        $paths = $this->getThemePaths($themes);
0 ignored issues
show
Bug introduced by
The variable $themes does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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