|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Module for ResourceLoader initialization. |
|
4
|
|
|
* |
|
5
|
|
|
* This program is free software; you can redistribute it and/or modify |
|
6
|
|
|
* it under the terms of the GNU General Public License as published by |
|
7
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
|
8
|
|
|
* (at your option) any later version. |
|
9
|
|
|
* |
|
10
|
|
|
* This program is distributed in the hope that it will be useful, |
|
11
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13
|
|
|
* GNU General Public License for more details. |
|
14
|
|
|
* |
|
15
|
|
|
* You should have received a copy of the GNU General Public License along |
|
16
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
|
17
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
18
|
|
|
* http://www.gnu.org/copyleft/gpl.html |
|
19
|
|
|
* |
|
20
|
|
|
* @file |
|
21
|
|
|
* @author Trevor Parscal |
|
22
|
|
|
* @author Roan Kattouw |
|
23
|
|
|
*/ |
|
24
|
|
|
|
|
25
|
|
|
class ResourceLoaderStartUpModule extends ResourceLoaderModule { |
|
26
|
|
|
|
|
27
|
|
|
// Cache for getConfigSettings() as it's called by multiple methods |
|
28
|
|
|
protected $configVars = []; |
|
29
|
|
|
protected $targets = [ 'desktop', 'mobile' ]; |
|
30
|
|
|
|
|
31
|
|
|
/** |
|
32
|
|
|
* @param ResourceLoaderContext $context |
|
33
|
|
|
* @return array |
|
34
|
|
|
*/ |
|
35
|
|
|
protected function getConfigSettings( $context ) { |
|
36
|
|
|
|
|
37
|
|
|
$hash = $context->getHash(); |
|
38
|
|
|
if ( isset( $this->configVars[$hash] ) ) { |
|
39
|
|
|
return $this->configVars[$hash]; |
|
40
|
|
|
} |
|
41
|
|
|
|
|
42
|
|
|
global $wgContLang; |
|
43
|
|
|
$conf = $this->getConfig(); |
|
44
|
|
|
|
|
45
|
|
|
// We can't use Title::newMainPage() if 'mainpage' is in |
|
46
|
|
|
// $wgForceUIMsgAsContentMsg because that will try to use the session |
|
47
|
|
|
// user's language and we have no session user. This does the |
|
48
|
|
|
// equivalent but falling back to our ResourceLoaderContext language |
|
49
|
|
|
// instead. |
|
50
|
|
|
$mainPage = Title::newFromText( $context->msg( 'mainpage' )->inContentLanguage()->text() ); |
|
51
|
|
|
if ( !$mainPage ) { |
|
52
|
|
|
$mainPage = Title::newFromText( 'Main Page' ); |
|
53
|
|
|
} |
|
54
|
|
|
|
|
55
|
|
|
/** |
|
56
|
|
|
* Namespace related preparation |
|
57
|
|
|
* - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces. |
|
58
|
|
|
* - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive. |
|
59
|
|
|
*/ |
|
60
|
|
|
$namespaceIds = $wgContLang->getNamespaceIds(); |
|
61
|
|
|
$caseSensitiveNamespaces = []; |
|
62
|
|
|
foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) { |
|
|
|
|
|
|
63
|
|
|
$namespaceIds[$wgContLang->lc( $name )] = $index; |
|
64
|
|
|
if ( !MWNamespace::isCapitalized( $index ) ) { |
|
65
|
|
|
$caseSensitiveNamespaces[] = $index; |
|
66
|
|
|
} |
|
67
|
|
|
} |
|
68
|
|
|
|
|
69
|
|
|
$illegalFileChars = $conf->get( 'IllegalFileChars' ); |
|
70
|
|
|
|
|
71
|
|
|
// Build list of variables |
|
72
|
|
|
$vars = [ |
|
73
|
|
|
'wgLoadScript' => wfScript( 'load' ), |
|
74
|
|
|
'debug' => $context->getDebug(), |
|
75
|
|
|
'skin' => $context->getSkin(), |
|
76
|
|
|
'stylepath' => $conf->get( 'StylePath' ), |
|
77
|
|
|
'wgUrlProtocols' => wfUrlProtocols(), |
|
78
|
|
|
'wgArticlePath' => $conf->get( 'ArticlePath' ), |
|
79
|
|
|
'wgScriptPath' => $conf->get( 'ScriptPath' ), |
|
80
|
|
|
'wgScriptExtension' => '.php', |
|
81
|
|
|
'wgScript' => wfScript(), |
|
82
|
|
|
'wgSearchType' => $conf->get( 'SearchType' ), |
|
83
|
|
|
'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ), |
|
84
|
|
|
// Force object to avoid "empty" associative array from |
|
85
|
|
|
// becoming [] instead of {} in JS (bug 34604) |
|
86
|
|
|
'wgActionPaths' => (object)$conf->get( 'ActionPaths' ), |
|
87
|
|
|
'wgServer' => $conf->get( 'Server' ), |
|
88
|
|
|
'wgServerName' => $conf->get( 'ServerName' ), |
|
89
|
|
|
'wgUserLanguage' => $context->getLanguage(), |
|
90
|
|
|
'wgContentLanguage' => $wgContLang->getCode(), |
|
91
|
|
|
'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ), |
|
92
|
|
|
'wgVersion' => $conf->get( 'Version' ), |
|
93
|
|
|
'wgEnableAPI' => $conf->get( 'EnableAPI' ), |
|
94
|
|
|
'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ), |
|
95
|
|
|
'wgMainPageTitle' => $mainPage->getPrefixedText(), |
|
96
|
|
|
'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(), |
|
97
|
|
|
'wgNamespaceIds' => $namespaceIds, |
|
98
|
|
|
'wgContentNamespaces' => MWNamespace::getContentNamespaces(), |
|
99
|
|
|
'wgSiteName' => $conf->get( 'Sitename' ), |
|
100
|
|
|
'wgDBname' => $conf->get( 'DBname' ), |
|
101
|
|
|
'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ), |
|
102
|
|
|
'wgAvailableSkins' => Skin::getSkinNames(), |
|
103
|
|
|
'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ), |
|
104
|
|
|
// MediaWiki sets cookies to have this prefix by default |
|
105
|
|
|
'wgCookiePrefix' => $conf->get( 'CookiePrefix' ), |
|
106
|
|
|
'wgCookieDomain' => $conf->get( 'CookieDomain' ), |
|
107
|
|
|
'wgCookiePath' => $conf->get( 'CookiePath' ), |
|
108
|
|
|
'wgCookieExpiration' => $conf->get( 'CookieExpiration' ), |
|
109
|
|
|
'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ), |
|
110
|
|
|
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, |
|
111
|
|
|
'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), |
|
112
|
|
|
'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ), |
|
113
|
|
|
'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ), |
|
114
|
|
|
'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), |
|
115
|
|
|
'wgResourceLoaderLegacyModules' => self::getLegacyModules(), |
|
116
|
|
|
'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ), |
|
117
|
|
|
'wgEnableUploads' => $conf->get( 'EnableUploads' ), |
|
118
|
|
|
]; |
|
119
|
|
|
|
|
120
|
|
|
Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] ); |
|
121
|
|
|
|
|
122
|
|
|
$this->configVars[$hash] = $vars; |
|
123
|
|
|
return $this->configVars[$hash]; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* Recursively get all explicit and implicit dependencies for to the given module. |
|
128
|
|
|
* |
|
129
|
|
|
* @param array $registryData |
|
130
|
|
|
* @param string $moduleName |
|
131
|
|
|
* @return array |
|
132
|
|
|
*/ |
|
133
|
|
|
protected static function getImplicitDependencies( array $registryData, $moduleName ) { |
|
134
|
|
|
static $dependencyCache = []; |
|
135
|
|
|
|
|
136
|
|
|
// The list of implicit dependencies won't be altered, so we can |
|
137
|
|
|
// cache them without having to worry. |
|
138
|
|
|
if ( !isset( $dependencyCache[$moduleName] ) ) { |
|
139
|
|
|
|
|
140
|
|
|
if ( !isset( $registryData[$moduleName] ) ) { |
|
141
|
|
|
// Dependencies may not exist |
|
142
|
|
|
$dependencyCache[$moduleName] = []; |
|
143
|
|
|
} else { |
|
144
|
|
|
$data = $registryData[$moduleName]; |
|
145
|
|
|
$dependencyCache[$moduleName] = $data['dependencies']; |
|
146
|
|
|
|
|
147
|
|
|
foreach ( $data['dependencies'] as $dependency ) { |
|
148
|
|
|
// Recursively get the dependencies of the dependencies |
|
149
|
|
|
$dependencyCache[$moduleName] = array_merge( |
|
150
|
|
|
$dependencyCache[$moduleName], |
|
151
|
|
|
self::getImplicitDependencies( $registryData, $dependency ) |
|
152
|
|
|
); |
|
153
|
|
|
} |
|
154
|
|
|
} |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
|
return $dependencyCache[$moduleName]; |
|
158
|
|
|
} |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* Optimize the dependency tree in $this->modules. |
|
162
|
|
|
* |
|
163
|
|
|
* The optimization basically works like this: |
|
164
|
|
|
* Given we have module A with the dependencies B and C |
|
165
|
|
|
* and module B with the dependency C. |
|
166
|
|
|
* Now we don't have to tell the client to explicitly fetch module |
|
167
|
|
|
* C as that's already included in module B. |
|
168
|
|
|
* |
|
169
|
|
|
* This way we can reasonably reduce the amount of module registration |
|
170
|
|
|
* data send to the client. |
|
171
|
|
|
* |
|
172
|
|
|
* @param array &$registryData Modules keyed by name with properties: |
|
173
|
|
|
* - string 'version' |
|
174
|
|
|
* - array 'dependencies' |
|
175
|
|
|
* - string|null 'group' |
|
176
|
|
|
* - string 'source' |
|
177
|
|
|
*/ |
|
178
|
|
|
public static function compileUnresolvedDependencies( array &$registryData ) { |
|
179
|
|
|
foreach ( $registryData as $name => &$data ) { |
|
180
|
|
|
$dependencies = $data['dependencies']; |
|
181
|
|
|
foreach ( $data['dependencies'] as $dependency ) { |
|
182
|
|
|
$implicitDependencies = self::getImplicitDependencies( $registryData, $dependency ); |
|
183
|
|
|
$dependencies = array_diff( $dependencies, $implicitDependencies ); |
|
184
|
|
|
} |
|
185
|
|
|
// Rebuild keys |
|
186
|
|
|
$data['dependencies'] = array_values( $dependencies ); |
|
187
|
|
|
} |
|
188
|
|
|
} |
|
189
|
|
|
|
|
190
|
|
|
/** |
|
191
|
|
|
* Get registration code for all modules. |
|
192
|
|
|
* |
|
193
|
|
|
* @param ResourceLoaderContext $context |
|
194
|
|
|
* @return string JavaScript code for registering all modules with the client loader |
|
195
|
|
|
*/ |
|
196
|
|
|
public function getModuleRegistrations( ResourceLoaderContext $context ) { |
|
197
|
|
|
|
|
198
|
|
|
$resourceLoader = $context->getResourceLoader(); |
|
199
|
|
|
$target = $context->getRequest()->getVal( 'target', 'desktop' ); |
|
200
|
|
|
// Bypass target filter if this request is Special:JavaScriptTest. |
|
201
|
|
|
// To prevent misuse in production, this is only allowed if testing is enabled server-side. |
|
202
|
|
|
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test'; |
|
203
|
|
|
|
|
204
|
|
|
$out = ''; |
|
205
|
|
|
$registryData = []; |
|
206
|
|
|
|
|
207
|
|
|
// Get registry data |
|
208
|
|
|
foreach ( $resourceLoader->getModuleNames() as $name ) { |
|
209
|
|
|
$module = $resourceLoader->getModule( $name ); |
|
210
|
|
|
$moduleTargets = $module->getTargets(); |
|
211
|
|
|
if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) { |
|
212
|
|
|
continue; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
if ( $module->isRaw() ) { |
|
216
|
|
|
// Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because |
|
217
|
|
|
// depending on them is illegal anyway and would only lead to them being reloaded |
|
218
|
|
|
// causing any state to be lost (like jQuery plugins, mw.config etc.) |
|
219
|
|
|
continue; |
|
220
|
|
|
} |
|
221
|
|
|
|
|
222
|
|
|
$versionHash = $module->getVersionHash( $context ); |
|
223
|
|
|
if ( strlen( $versionHash ) !== 7 ) { |
|
224
|
|
|
$context->getLogger()->warning( |
|
225
|
|
|
"Module '{module}' produced an invalid version hash: '{version}'.", |
|
226
|
|
|
[ |
|
227
|
|
|
'module' => $name, |
|
228
|
|
|
'version' => $versionHash, |
|
229
|
|
|
] |
|
230
|
|
|
); |
|
231
|
|
|
// Module implementation either broken or deviated from ResourceLoader::makeHash |
|
232
|
|
|
// Asserted by tests/phpunit/structure/ResourcesTest. |
|
233
|
|
|
$versionHash = ResourceLoader::makeHash( $versionHash ); |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
$skipFunction = $module->getSkipFunction(); |
|
237
|
|
|
if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) { |
|
238
|
|
|
$skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction ); |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
$registryData[$name] = [ |
|
242
|
|
|
'version' => $versionHash, |
|
243
|
|
|
'dependencies' => $module->getDependencies( $context ), |
|
244
|
|
|
'group' => $module->getGroup(), |
|
245
|
|
|
'source' => $module->getSource(), |
|
246
|
|
|
'skip' => $skipFunction, |
|
247
|
|
|
]; |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
self::compileUnresolvedDependencies( $registryData ); |
|
251
|
|
|
|
|
252
|
|
|
// Register sources |
|
253
|
|
|
$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() ); |
|
254
|
|
|
|
|
255
|
|
|
// Figure out the different call signatures for mw.loader.register |
|
256
|
|
|
$registrations = []; |
|
257
|
|
|
foreach ( $registryData as $name => $data ) { |
|
258
|
|
|
// Call mw.loader.register(name, version, dependencies, group, source, skip) |
|
259
|
|
|
$registrations[] = [ |
|
260
|
|
|
$name, |
|
261
|
|
|
$data['version'], |
|
262
|
|
|
$data['dependencies'], |
|
263
|
|
|
$data['group'], |
|
264
|
|
|
// Swap default (local) for null |
|
265
|
|
|
$data['source'] === 'local' ? null : $data['source'], |
|
266
|
|
|
$data['skip'] |
|
267
|
|
|
]; |
|
268
|
|
|
} |
|
269
|
|
|
|
|
270
|
|
|
// Register modules |
|
271
|
|
|
$out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations ); |
|
272
|
|
|
|
|
273
|
|
|
return $out; |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
/** |
|
277
|
|
|
* @return bool |
|
278
|
|
|
*/ |
|
279
|
|
|
public function isRaw() { |
|
280
|
|
|
return true; |
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
/** |
|
284
|
|
|
* Base modules required for the base environment of ResourceLoader |
|
285
|
|
|
* |
|
286
|
|
|
* @return array |
|
287
|
|
|
*/ |
|
288
|
|
|
public static function getStartupModules() { |
|
289
|
|
|
return [ 'jquery', 'mediawiki' ]; |
|
290
|
|
|
} |
|
291
|
|
|
|
|
292
|
|
|
public static function getLegacyModules() { |
|
293
|
|
|
global $wgIncludeLegacyJavaScript; |
|
294
|
|
|
|
|
295
|
|
|
$legacyModules = []; |
|
296
|
|
|
if ( $wgIncludeLegacyJavaScript ) { |
|
297
|
|
|
$legacyModules[] = 'mediawiki.legacy.wikibits'; |
|
298
|
|
|
} |
|
299
|
|
|
|
|
300
|
|
|
return $legacyModules; |
|
301
|
|
|
} |
|
302
|
|
|
|
|
303
|
|
|
/** |
|
304
|
|
|
* Get the load URL of the startup modules. |
|
305
|
|
|
* |
|
306
|
|
|
* This is a helper for getScript(), but can also be called standalone, such |
|
307
|
|
|
* as when generating an AppCache manifest. |
|
308
|
|
|
* |
|
309
|
|
|
* @param ResourceLoaderContext $context |
|
310
|
|
|
* @return string |
|
311
|
|
|
*/ |
|
312
|
|
|
public static function getStartupModulesUrl( ResourceLoaderContext $context ) { |
|
313
|
|
|
$rl = $context->getResourceLoader(); |
|
314
|
|
|
|
|
315
|
|
|
$derivative = new DerivativeResourceLoaderContext( $context ); |
|
316
|
|
|
$derivative->setModules( self::getStartupModules() ); |
|
317
|
|
|
$derivative->setOnly( 'scripts' ); |
|
318
|
|
|
// Must setModules() before makeVersionQuery() |
|
319
|
|
|
$derivative->setVersion( $rl->makeVersionQuery( $derivative ) ); |
|
|
|
|
|
|
320
|
|
|
|
|
321
|
|
|
return $rl->createLoaderURL( 'local', $derivative ); |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
/** |
|
325
|
|
|
* @param ResourceLoaderContext $context |
|
326
|
|
|
* @return string |
|
327
|
|
|
*/ |
|
328
|
|
|
public function getScript( ResourceLoaderContext $context ) { |
|
329
|
|
|
global $IP; |
|
330
|
|
|
if ( $context->getOnly() !== 'scripts' ) { |
|
331
|
|
|
return '/* Requires only=script */'; |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
|
$out = file_get_contents( "$IP/resources/src/startup.js" ); |
|
335
|
|
|
|
|
336
|
|
|
$pairs = array_map( function ( $value ) { |
|
337
|
|
|
$value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK ); |
|
338
|
|
|
// Fix indentation |
|
339
|
|
|
$value = str_replace( "\n", "\n\t", $value ); |
|
340
|
|
|
return $value; |
|
341
|
|
|
}, [ |
|
342
|
|
|
'$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), |
|
343
|
|
|
'$VARS.configuration' => $this->getConfigSettings( $context ), |
|
344
|
|
|
'$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ), |
|
345
|
|
|
] ); |
|
346
|
|
|
$pairs['$CODE.registrations()'] = str_replace( |
|
347
|
|
|
"\n", |
|
348
|
|
|
"\n\t", |
|
349
|
|
|
trim( $this->getModuleRegistrations( $context ) ) |
|
350
|
|
|
); |
|
351
|
|
|
|
|
352
|
|
|
return strtr( $out, $pairs ); |
|
353
|
|
|
} |
|
354
|
|
|
|
|
355
|
|
|
/** |
|
356
|
|
|
* @return bool |
|
357
|
|
|
*/ |
|
358
|
|
|
public function supportsURLLoading() { |
|
359
|
|
|
return false; |
|
360
|
|
|
} |
|
361
|
|
|
|
|
362
|
|
|
/** |
|
363
|
|
|
* Get the definition summary for this module. |
|
364
|
|
|
* |
|
365
|
|
|
* @param ResourceLoaderContext $context |
|
366
|
|
|
* @return array |
|
367
|
|
|
*/ |
|
368
|
|
|
public function getDefinitionSummary( ResourceLoaderContext $context ) { |
|
369
|
|
|
global $IP; |
|
370
|
|
|
$summary = parent::getDefinitionSummary( $context ); |
|
371
|
|
|
$summary[] = [ |
|
372
|
|
|
// Detect changes to variables exposed in mw.config (T30899). |
|
373
|
|
|
'vars' => $this->getConfigSettings( $context ), |
|
374
|
|
|
// Changes how getScript() creates mw.Map for mw.config |
|
375
|
|
|
'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), |
|
376
|
|
|
// Detect changes to the module registrations |
|
377
|
|
|
'moduleHashes' => $this->getAllModuleHashes( $context ), |
|
378
|
|
|
|
|
379
|
|
|
'fileMtimes' => [ |
|
380
|
|
|
filemtime( "$IP/resources/src/startup.js" ), |
|
381
|
|
|
], |
|
382
|
|
|
]; |
|
383
|
|
|
return $summary; |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
|
|
/** |
|
387
|
|
|
* Helper method for getDefinitionSummary(). |
|
388
|
|
|
* |
|
389
|
|
|
* @param ResourceLoaderContext $context |
|
390
|
|
|
* @return string SHA-1 |
|
391
|
|
|
*/ |
|
392
|
|
|
protected function getAllModuleHashes( ResourceLoaderContext $context ) { |
|
393
|
|
|
$rl = $context->getResourceLoader(); |
|
394
|
|
|
// Preload for getCombinedVersion() |
|
395
|
|
|
$rl->preloadModuleInfo( $rl->getModuleNames(), $context ); |
|
396
|
|
|
|
|
397
|
|
|
// ATTENTION: Because of the line below, this is not going to cause infinite recursion. |
|
398
|
|
|
// Think carefully before making changes to this code! |
|
399
|
|
|
// Pre-populate versionHash with something because the loop over all modules below includes |
|
400
|
|
|
// the startup module (this module). |
|
401
|
|
|
// See ResourceLoaderModule::getVersionHash() for usage of this cache. |
|
402
|
|
|
$this->versionHash[$context->getHash()] = null; |
|
403
|
|
|
|
|
404
|
|
|
return $rl->getCombinedVersion( $context, $rl->getModuleNames() ); |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
/** |
|
408
|
|
|
* @return string |
|
409
|
|
|
*/ |
|
410
|
|
|
public function getGroup() { |
|
411
|
|
|
return 'startup'; |
|
412
|
|
|
} |
|
413
|
|
|
} |
|
414
|
|
|
|
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.