Completed
Push — master ( e62549...d4a329 )
by Jeroen De
13s queued 11s
created

ResourceLoaderSCSSModule   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 89
c 1
b 0
f 0
dl 0
loc 256
rs 9.84
wmc 32

14 Methods

Rating   Name   Duplication   Size   Complexity  
A supportsURLLoading() 0 2 1
A getCache() 0 7 2
A __construct() 0 5 1
A getCacheKey() 0 22 2
A getCacheType() 0 2 2
A compileStyles() 0 45 5
A applyOptions() 0 11 3
A setCache() 0 2 1
A purgeCache() 0 2 1
A retrieveStylesFromCache() 0 17 3
A getStyles() 0 12 3
A isCacheOutdated() 0 11 4
A updateCache() 0 5 1
A getStyleFilesList() 0 11 3
1
<?php
2
/**
3
 * File containing the ResourceLoaderSCSSModule class
4
 *
5
 * @copyright 2018 - 2019, Stephan Gambke
6
 * @license   GNU General Public License, version 3 (or any later version)
7
 *
8
 * This file is part of the MediaWiki extension SCSS.
9
 * The SCSS extension is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by the Free
11
 * Software Foundation, either version 3 of the License, or (at your option) any
12
 * later version.
13
 *
14
 * The SCSS extension is distributed in the hope that it will be useful, but
15
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17
 * details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program. If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 * @file
23
 * @ingroup SCSS
24
 */
25
26
namespace SCSS;
27
28
use BagOStuff;
0 ignored issues
show
Bug introduced by
The type BagOStuff was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use CSSJanus;
0 ignored issues
show
Bug introduced by
The type CSSJanus was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use Exception;
31
use ScssPhp\ScssPhp\Compiler;
0 ignored issues
show
Bug introduced by
The type ScssPhp\ScssPhp\Compiler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
use ObjectCache;
0 ignored issues
show
Bug introduced by
The type ObjectCache was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use ResourceLoaderContext;
0 ignored issues
show
Bug introduced by
The type ResourceLoaderContext was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
use ResourceLoaderFileModule;
0 ignored issues
show
Bug introduced by
The type ResourceLoaderFileModule was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
36
37
/**
38
 * ResourceLoader module based on local JavaScript/SCSS files.
39
 *
40
 * It recognizes the following additional fields in $wgResourceModules:
41
 * * styles: array of SCSS file names (with or without extension .scss)
42
 * * variables: array of key value pairs representing SCSS variables, that will
43
 *              be added to the SCSS script after all files imports, i.e. that
44
 *              may override any variable set in style files
45
 * * paths: array of paths to search for style files; all these paths together
46
 *              represent one virtual file base and will be searched for a style
47
 *              file; this means it is not possible to include two SCSS files
48
 *              with the same name even if in different paths
49
 *
50
 * @ingroup SCSS
51
 */
52
class ResourceLoaderSCSSModule extends ResourceLoaderFileModule {
53
54
	private $styleModulePositions = [
55
		'beforeFunctions', 'functions', 'afterFunctions',
56
		'beforeVariables', 'variables', 'afterVariables',
57
		'beforeMain', 'main', 'afterMain',
58
	];
59
60
	private $cache = null;
61
	private $cacheKey = null;
62
63
	protected $variables = [];
64
	protected $paths = [];
65
	protected $cacheTriggers = [];
66
67
	protected $styleText = null;
68
69
	/**
70
	 * ResourceLoaderSCSSModule constructor.
71
	 *
72
	 * @param mixed[] $options
73
	 * @param string|null $localBasePath
74
	 * @param string|null $remoteBasePath
75
	 */
76
	public function __construct( $options = [], $localBasePath = null, $remoteBasePath = null ) {
77
78
		parent::__construct( $options, $localBasePath, $remoteBasePath );
79
80
		$this->applyOptions( $options );
81
	}
82
83
	/**
84
	 * @param mixed[] $options
85
	 */
86
	protected function applyOptions( $options ) {
87
88
		$mapConfigToLocalVar = [
89
			'variables'      => 'variables',
90
			'paths'          => 'paths',
91
			'cacheTriggers' => 'cacheTriggers',
92
		];
93
94
		foreach ( $mapConfigToLocalVar as $config => $local ) {
95
			if ( isset( $options[ $config ] ) ) {
96
				$this->$local = $options[ $config ];
97
			}
98
		}
99
	}
100
101
	/**
102
	 * Get the compiled Bootstrap styles
103
	 *
104
	 * @param ResourceLoaderContext $context
105
	 *
106
	 * @return array
107
	 */
108
	public function getStyles( ResourceLoaderContext $context ) {
109
110
		if ( $this->styleText === null ) {
111
112
			$this->retrieveStylesFromCache( $context );
113
114
			if ( $this->styleText === null ) {
115
				$this->compileStyles( $context );
116
			}
117
		}
118
119
		return [ 'all' => $this->styleText ];
120
	}
121
122
	/**
123
	 * @param ResourceLoaderContext $context
124
	 */
125
	protected function retrieveStylesFromCache( ResourceLoaderContext $context ) {
126
127
		// Try for cache hit
128
		$cacheKey = $this->getCacheKey( $context );
129
		$cacheResult = $this->getCache()->get( $cacheKey );
130
131
		if ( is_array( $cacheResult ) ) {
132
133
			if ( $this->isCacheOutdated( $cacheResult[ 'storetime' ] ) ) {
134
				wfDebug( "SCSS: Cache miss for {$this->getName()}: Cache outdated.\n", 'private' );
0 ignored issues
show
Bug introduced by
The function wfDebug was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

134
				/** @scrutinizer ignore-call */ 
135
    wfDebug( "SCSS: Cache miss for {$this->getName()}: Cache outdated.\n", 'private' );
Loading history...
135
			} else {
136
				$this->styleText = $cacheResult[ 'styles' ];
137
				wfDebug( "SCSS: Cache hit for {$this->getName()}: Got styles from cache.\n", 'private' );
138
			}
139
140
		} else {
141
			wfDebug( "SCSS: Cache miss for {$this->getName()}: Styles not found in cache.\n", 'private' );
142
		}
143
	}
144
145
	/**
146
	 * @return BagOStuff|null
147
	 */
148
	protected function getCache() {
149
150
		if ( $this->cache === null ) {
151
			$this->cache = ObjectCache::getInstance( $this->getCacheType() );
152
		}
153
154
		return $this->cache;
155
	}
156
157
	private function getCacheType() {
158
		return array_key_exists( 'egScssCacheType', $GLOBALS ) ? $GLOBALS[ 'egScssCacheType' ] : -1;
159
	}
160
161
	/**
162
	 * @since  1.0
163
	 *
164
	 * @param BagOStuff $cache
165
	 */
166
	public function setCache( BagOStuff $cache ) {
167
		$this->cache = $cache;
168
	}
169
170
	/**
171
	 * @param ResourceLoaderContext $context
172
	 *
173
	 * @return string
174
	 */
175
	protected function getCacheKey( ResourceLoaderContext $context ) {
176
177
		if ( $this->cacheKey === null ) {
178
179
			$styles = serialize( $this->styles );
180
181
			$vars = $this->variables;
182
			ksort( $vars );
183
			$vars = serialize( $vars );
184
185
			// have to hash the module config, else it may become too long
186
			$configHash = md5( $styles . $vars );
187
188
			$this->cacheKey = wfMemcKey(
0 ignored issues
show
Bug introduced by
The function wfMemcKey was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

188
			$this->cacheKey = /** @scrutinizer ignore-call */ wfMemcKey(
Loading history...
189
				'ext',
190
				'scss',
191
				$configHash,
192
				$context->getDirection()
193
			);
194
		}
195
196
		return $this->cacheKey;
197
	}
198
199
	/**
200
	 * @param int $cacheStoreTime
201
	 *
202
	 * @return bool
203
	 */
204
	protected function isCacheOutdated( $cacheStoreTime ) {
205
206
		foreach ( $this->cacheTriggers as $triggerFile ) {
207
208
			if ( $triggerFile !== null && $cacheStoreTime < filemtime( $triggerFile ) ) {
209
				return true;
210
			}
211
212
		}
213
214
		return false;
215
	}
216
217
	/**
218
	 * @param ResourceLoaderContext $context
219
	 */
220
	protected function compileStyles( ResourceLoaderContext $context ) {
221
222
		$scss = new Compiler();
223
		$scss->setImportPaths( $this->getLocalPath( '' ) );
224
225
		// Allows inlining of arbitrary files regardless of extension, .css in particular
226
		$scss->addImportPath(
227
228
			// addImportPath is declared as requiring a string param, but actually also understand callables
229
			/** @scrutinizer ignore-type */
230
			function ( $path ) {
231
				if ( file_exists( $path ) ) {
232
					return $path;
233
				}
234
				return null;
235
			}
236
237
		);
238
239
		try {
240
241
			$imports = $this->getStyleFilesList();
242
243
			foreach ( $imports as $key => $import ) {
244
				$path = str_replace( [ '\\', '"' ], [ '\\\\', '\\"' ], $import );
245
				$imports[ $key ] = '@import "' . $path . '";';
246
			}
247
248
			$scss->setVariables( $this->variables );
249
250
			$style = $scss->compile( implode( $imports ) );
251
252
			if ( $this->getFlip( $context ) ) {
253
				$style = CSSJanus::transform( $style, true, false );
254
			}
255
256
			$this->styleText = $style;
257
258
			$this->updateCache( $context );
259
260
		} catch ( Exception $e ) {
261
262
			$this->purgeCache( $context );
263
			wfDebug( $e->getMessage() );
0 ignored issues
show
Bug introduced by
The function wfDebug was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

263
			/** @scrutinizer ignore-call */ 
264
   wfDebug( $e->getMessage() );
Loading history...
264
			$this->styleText = '/* SCSS compile error: ' . $e->getMessage() . '*/';
265
		}
266
267
	}
268
269
	/**
270
	 * @param ResourceLoaderContext $context
271
	 */
272
	protected function updateCache( ResourceLoaderContext $context ) {
273
274
		$this->getCache()->set(
275
			$this->getCacheKey( $context ),
276
			[ 'styles' => $this->styleText, 'storetime' => time() ]
277
		);
278
	}
279
280
	/**
281
	 * @param ResourceLoaderContext $context
282
	 */
283
	protected function purgeCache( ResourceLoaderContext $context ) {
284
		$this->getCache()->delete( $this->getCacheKey( $context ) );
285
	}
286
287
	/**
288
	 * @see ResourceLoaderFileModule::supportsURLLoading
289
	 */
290
	public function supportsURLLoading() {
291
		return false;
292
	}
293
294
	/**
295
	 * @return array
296
	 */
297
	protected function getStyleFilesList() {
298
		$styles = self::collateFilePathListByOption( $this->styles, 'position', 'main' );
299
		$imports = [];
300
301
		foreach ( $this->styleModulePositions as $position ) {
302
			if ( isset( $styles[ $position ] ) ) {
303
				$imports = array_merge( $imports, $styles[ $position ] );
304
			}
305
		}
306
307
		return $imports;
308
	}
309
310
}
311