Passed
Push — master ( 202a73...bb5081 )
by Fabio
06:38
created

TTheme::setName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * TThemeManager class
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 * @package Prado\Web\UI
9
 */
10
11
namespace Prado\Web\UI;
12
13
use Prado\Exceptions\TIOException;
14
use Prado\Exceptions\TConfigurationException;
15
use Prado\Prado;
16
use Prado\TApplicationMode;
17
18
/**
19
 * TTheme class
20
 *
21
 * TTheme represents a particular theme. It is merely a collection of skins
22
 * that are applicable to the corresponding controls.
23
 *
24
 * Each theme is stored as a directory and files under that directory.
25
 * The theme name is the directory name. When TTheme is created, the files
26
 * whose name has the extension ".skin" are parsed and saved as controls skins.
27
 *
28
 * A skin is essentially a list of initial property values that are to be applied
29
 * to a control when the skin is applied.
30
 * Each type of control can have multiple skins identified by the SkinID.
31
 * If a skin does not have SkinID, it is the default skin that will be applied
32
 * to controls that do not specify particular SkinID.
33
 *
34
 * For globalization, Themes remove all css files that end in ".*rtl.css" or
35
 * ".*rtl.(media).css".  Then if there is Application Globalization, and
36
 * globalization current culture is Right-to-left then the rtl css files are
37
 * re-added at the end of the css files so rtl css has priority.
38
 *
39
 * Whenever possible, TTheme will try to make use of available cache to save
40
 * the parsing time.
41
 *
42
 * To apply a theme to a particular control, call {@link applySkin}.
43
 *
44
 * @author Qiang Xue <[email protected]>
45
 * @package Prado\Web\UI
46
 * @since 3.0
47
 */
48
class TTheme extends \Prado\TApplicationComponent implements ITheme
49
{
50
	/**
51
	 * prefix for cache variable name used to store parsed themes
52
	 */
53
	const THEME_CACHE_PREFIX = 'prado:theme:';
54
	/**
55
	 * Extension name of skin files
56
	 */
57
	const SKIN_FILE_EXT = '.skin';
58
	/**
59
	 * @var string theme path
60
	 */
61
	private $_themePath;
62
	/**
63
	 * @var string theme url
64
	 */
65
	private $_themeUrl;
66
	/**
67
	 * @var array list of skins for the theme
68
	 */
69
	private $_skins;
70
	/**
71
	 * @var string theme name
72
	 */
73
	private $_name = '';
74
	/**
75
	 * @var array list of css files
76
	 */
77
	private $_cssFiles = [];
78
	/**
79
	 * @var array list of js files
80
	 */
81
	private $_jsFiles = [];
82
83
	/**
84
	 * Constructor.
85
	 * @param string $themePath theme path
86
	 * @param string $themeUrl theme URL
87
	 * @throws TConfigurationException if theme path does not exist or any parsing error of the skin files
88
	 */
89
	public function __construct($themePath, $themeUrl)
90
	{
91
		$this->_themeUrl = $themeUrl;
92
		$this->_themePath = realpath($themePath);
93
		$this->_name = basename($themePath);
94
		parent::__construct();
95
		$cacheValid = false;
96
		// TODO: the following needs to be cleaned up (Qiang)
97
		if (($cache = $this->getApplication()->getCache()) !== null) {
98
			$array = $cache->get(self::THEME_CACHE_PREFIX . $themePath);
99
			if (is_array($array)) {
100
				[$skins, $cssFiles, $jsFiles, $timestamp] = $array;
101
				if ($this->getApplication()->getMode() !== TApplicationMode::Performance) {
102
					if (($dir = opendir($themePath)) === false) {
103
						throw new TIOException('theme_path_inexistent', $themePath);
104
					}
105
					$cacheValid = true;
106
					while (($file = readdir($dir)) !== false) {
107
						if ($file === '.' || $file === '..') {
108
							continue;
109
						} elseif (basename($file, '.css') !== $file) {
110
							$this->_cssFiles[] = $themeUrl . DIRECTORY_SEPARATOR . $file;
111
						} elseif (basename($file, '.js') !== $file) {
112
							$this->_jsFiles[] = $themeUrl . DIRECTORY_SEPARATOR . $file;
113
						} elseif (basename($file, self::SKIN_FILE_EXT) !== $file && filemtime($themePath . DIRECTORY_SEPARATOR . $file) > $timestamp) {
114
							$cacheValid = false;
115
							break;
116
						}
117
					}
118
					closedir($dir);
119
					if ($cacheValid) {
120
						$this->_skins = $skins;
121
					}
122
				} else {
123
					$cacheValid = true;
124
					$this->_cssFiles = $cssFiles;
125
					$this->_jsFiles = $jsFiles;
126
					$this->_skins = $skins;
127
				}
128
			}
129
		}
130
		if (!$cacheValid) {
131
			$this->_cssFiles = [];
132
			$this->_jsFiles = [];
133
			$this->_skins = [];
134
			if (($dir = opendir($themePath)) === false) {
135
				throw new TIOException('theme_path_inexistent', $themePath);
136
			}
137
			while (($file = readdir($dir)) !== false) {
138
				if ($file === '.' || $file === '..') {
139
					continue;
140
				} elseif (basename($file, '.css') !== $file) {
141
					$this->_cssFiles[] = $themeUrl . DIRECTORY_SEPARATOR . $file;
142
				} elseif (basename($file, '.js') !== $file) {
143
					$this->_jsFiles[] = $themeUrl . DIRECTORY_SEPARATOR . $file;
144
				} elseif (basename($file, self::SKIN_FILE_EXT) !== $file) {
145
					$template = new TTemplate(file_get_contents($themePath . DIRECTORY_SEPARATOR . $file), $themePath, $themePath . '/' . $file);
146
					foreach ($template->getItems() as $skin) {
147
						if (!isset($skin[2])) {  // a text string, ignored
148
							continue;
149
						} elseif ($skin[0] !== -1) {
150
							throw new TConfigurationException('theme_control_nested', $skin[1], dirname($themePath));
151
						}
152
						$type = $skin[1];
153
						$id = $skin[2]['skinid'] ?? 0;
154
						unset($skin[2]['skinid']);
155
						if (isset($this->_skins[$type][$id])) {
156
							throw new TConfigurationException('theme_skinid_duplicated', $type, $id, dirname($themePath));
157
						}
158
						/*
159
						foreach($skin[2] as $name=>$value)
160
						{
161
							if(is_array($value) && ($value[0]===TTemplate::CONFIG_DATABIND || $value[0]===TTemplate::CONFIG_PARAMETER))
162
								throw new TConfigurationException('theme_databind_forbidden',dirname($themePath),$type,$id);
163
						}
164
						*/
165
						$this->_skins[$type][$id] = $skin[2];
166
					}
167
				}
168
			}
169
			closedir($dir);
170
			sort($this->_cssFiles);
171
			sort($this->_jsFiles);
172
			$this->postProcessCssRTL();
173
			if ($cache !== null) {
174
				$cache->set(self::THEME_CACHE_PREFIX . $themePath, [$this->_skins, $this->_cssFiles, $this->_jsFiles, time()]);
175
			}
176
		}
177
	}
178
179
	/**
180
	 * @return string theme name
181
	 */
182
	public function getName()
183
	{
184
		return $this->_name;
185
	}
186
187
	/**
188
	 * @param string $value theme name
189
	 */
190
	protected function setName($value)
191
	{
192
		$this->_name = $value;
193
	}
194
195
	/**
196
	 * @return string the URL to the theme folder (without ending slash)
197
	 */
198
	public function getBaseUrl()
199
	{
200
		return $this->_themeUrl;
201
	}
202
203
	/**
204
	 * @param string $value the URL to the theme folder
205
	 */
206
	protected function setBaseUrl($value)
207
	{
208
		$this->_themeUrl = rtrim($value, DIRECTORY_SEPARATOR);
209
	}
210
211
	/**
212
	 * @return string the file path to the theme folder
213
	 */
214
	public function getBasePath()
215
	{
216
		return $this->_themePath;
217
	}
218
219
	/**
220
	 * @param string $value tthe file path to the theme folder
221
	 */
222
	protected function setBasePath($value)
223
	{
224
		$this->_themePath = $value;
225
	}
226
227
	/**
228
	 * @return array list of skins for the theme
229
	 */
230
	public function getSkins()
231
	{
232
		return $this->_skins;
233
	}
234
235
	/**
236
	 * @param array $value list of skins for the theme
237
	 */
238
	protected function setSkins($value)
239
	{
240
		$this->_skins = $value;
241
	}
242
243
	/**
244
	 * Applies the theme to a particular control.
245
	 * The control's class name and SkinID value will be used to
246
	 * identify which skin to be applied. If the control's SkinID is empty,
247
	 * the default skin will be applied.
248
	 * @param \Prado\Web\UI\TControl $control the control to be applied with a skin
249
	 * @throws TConfigurationException if any error happened during the skin application
250
	 * @return bool if a skin is successfully applied
251
	 */
252
	public function applySkin($control)
253
	{
254
		$type = get_class($control);
255
		if (($id = $control->getSkinID()) === '') {
256
			$id = 0;
257
		}
258
		if (isset($this->_skins[$type][$id])) {
259
			foreach ($this->_skins[$type][$id] as $name => $value) {
260
				Prado::trace("Applying skin $name to $type", 'Prado\Web\UI\TThemeManager');
261
				if (is_array($value)) {
262
					switch ($value[0]) {
263
						case TTemplate::CONFIG_EXPRESSION:
264
							$value = $this->evaluateExpression($value[1]);
265
							break;
266
						case TTemplate::CONFIG_ASSET:
267
							$value = $this->_themeUrl . '/' . ltrim($value[1], '/');
268
							break;
269
						case TTemplate::CONFIG_DATABIND:
270
							$control->bindProperty($name, $value[1]);
271
							break;
272
						case TTemplate::CONFIG_PARAMETER:
273
							$control->setSubProperty($name, $this->getApplication()->getParameters()->itemAt($value[1]));
274
							break;
275
						case TTemplate::CONFIG_TEMPLATE:
276
							$control->setSubProperty($name, $value[1]);
277
							break;
278
						case TTemplate::CONFIG_LOCALIZATION:
279
							$control->setSubProperty($name, Prado::localize($value[1]));
280
							break;
281
						default:
282
							throw new TConfigurationException('theme_tag_unexpected', $name, $value[0]);
283
							break;
284
					}
285
				}
286
				if (!is_array($value)) {
287
					if (strpos($name, '.') === false) {	// is simple property or custom attribute
288
						if ($control->hasProperty($name)) {
289
							if ($control->canSetProperty($name)) {
290
								$setter = 'set' . $name;
291
								$control->$setter($value);
292
							} else {
293
								throw new TConfigurationException('theme_property_readonly', $type, $name);
294
							}
295
						} else {
296
							throw new TConfigurationException('theme_property_undefined', $type, $name);
297
						}
298
					} else {	// complex property
299
						$control->setSubProperty($name, $value);
300
					}
301
				}
302
			}
303
			return true;
304
		} else {
305
			return false;
306
		}
307
	}
308
309
	/**
310
	 * @return array list of CSS files (URL) in the theme
311
	 */
312
	public function getStyleSheetFiles()
313
	{
314
		return $this->_cssFiles;
315
	}
316
317
	/**
318
	 * @param array $value list of CSS files (URL) in the theme
319
	 */
320
	protected function setStyleSheetFiles($value)
321
	{
322
		$this->_cssFiles = $value;
323
	}
324
325
	/**
326
	 * @return array list of Javascript files (URL) in the theme
327
	 */
328
	public function getJavaScriptFiles()
329
	{
330
		return $this->_jsFiles;
331
	}
332
333
	/**
334
	 * @param array $value list of Javascript files (URL) in the theme
335
	 */
336
	protected function setJavaScriptFiles($value)
337
	{
338
		$this->_jsFiles = $value;
339
	}
340
	
341
	/**
342
	 * This post-processes the theme for RTL.  It loops through all the
343
	 * css files, removes all the '.*rtl.css' and '.*rtl.(media).css' files,
344
	 * then if there is globalization and globalization is RTL then re-adds
345
	 * the rtl css at the end.
346
	 * @since 4.2.0
347
	 */
348
	protected function postProcessCssRTL()
349
	{
350
		$rtlCss = [];
351
		foreach ($this->_cssFiles as $key => $url) {
352
			$segs = explode('.', basename($url));
353
			if ((substr_compare(strtolower($url), 'rtl.css', -7) === 0) ||
354
				(isset($segs[2]) && (substr_compare(strtolower($segs[count($segs) - 3]), 'rtl', -3) === 0))) {
355
				$rtlCss[] = $url;
356
				unset($this->_cssFiles[$key]);
357
			}
358
		}
359
		if ($globalization = $this->getApplication()->getGlobalization()) {
360
			if ($globalization->getIsCultureRTL()) {
361
				$this->_cssFiles = array_merge($this->_cssFiles, $rtlCss);
362
			}
363
		}
364
	}
365
}
366