Completed
Push — master ( c8fdc7...6f28ac )
by Daniel
30s
created

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