Completed
Push — master ( 04785f...5fcb42 )
by Jeroen De
26s queued 11s
created

ResourceLoaderSCSSModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 5
rs 10
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 Bootstrap 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 Bootstrap
24
 */
25
26
namespace Bootstrap;
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 Leafo\ScssPhp\Compiler;
0 ignored issues
show
Bug introduced by
The type Leafo\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 Bootstrap
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( $GLOBALS[ 'egScssCacheType' ] );
152
		}
153
154
		return $this->cache;
155
	}
156
157
	/**
158
	 * @since  1.0
159
	 *
160
	 * @param BagOStuff $cache
161
	 */
162
	public function setCache( BagOStuff $cache ) {
163
		$this->cache = $cache;
164
	}
165
166
	/**
167
	 * @param ResourceLoaderContext $context
168
	 *
169
	 * @return string
170
	 */
171
	protected function getCacheKey( ResourceLoaderContext $context ) {
172
173
		if ( $this->cacheKey === null ) {
174
175
			$styles = serialize( $this->styles );
176
177
			$vars = $this->variables;
178
			ksort( $vars );
179
			$vars = serialize( $vars );
180
181
			// have to hash the module config, else it may become too long
182
			$configHash = md5( $styles . $vars );
183
184
			$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

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

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