These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Cache of the contents of localisation files. |
||
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 | */ |
||
22 | |||
23 | use Cdb\Reader as CdbReader; |
||
0 ignored issues
–
show
|
|||
24 | use Cdb\Writer as CdbWriter; |
||
0 ignored issues
–
show
This use statement conflicts with another class in this namespace,
CdbWriter .
Let’s assume that you have a directory layout like this: .
|-- OtherDir
| |-- Bar.php
| `-- Foo.php
`-- SomeDir
`-- Foo.php
and let’s assume the following content of // Bar.php
namespace OtherDir;
use SomeDir\Foo; // This now conflicts the class OtherDir\Foo
If both files PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as // Bar.php
namespace OtherDir;
use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
|
|||
25 | use CLDRPluralRuleParser\Evaluator; |
||
26 | use CLDRPluralRuleParser\Error as CLDRPluralRuleError; |
||
27 | use MediaWiki\MediaWikiServices; |
||
28 | |||
29 | /** |
||
30 | * Class for caching the contents of localisation files, Messages*.php |
||
31 | * and *.i18n.php. |
||
32 | * |
||
33 | * An instance of this class is available using Language::getLocalisationCache(). |
||
34 | * |
||
35 | * The values retrieved from here are merged, containing items from extension |
||
36 | * files, core messages files and the language fallback sequence (e.g. zh-cn -> |
||
37 | * zh-hans -> en ). Some common errors are corrected, for example namespace |
||
38 | * names with spaces instead of underscores, but heavyweight processing, such |
||
39 | * as grammatical transformation, is done by the caller. |
||
40 | */ |
||
41 | class LocalisationCache { |
||
42 | const VERSION = 4; |
||
43 | |||
44 | /** Configuration associative array */ |
||
45 | private $conf; |
||
46 | |||
47 | /** |
||
48 | * True if recaching should only be done on an explicit call to recache(). |
||
49 | * Setting this reduces the overhead of cache freshness checking, which |
||
50 | * requires doing a stat() for every extension i18n file. |
||
51 | */ |
||
52 | private $manualRecache = false; |
||
53 | |||
54 | /** |
||
55 | * True to treat all files as expired until they are regenerated by this object. |
||
56 | */ |
||
57 | private $forceRecache = false; |
||
58 | |||
59 | /** |
||
60 | * The cache data. 3-d array, where the first key is the language code, |
||
61 | * the second key is the item key e.g. 'messages', and the third key is |
||
62 | * an item specific subkey index. Some items are not arrays and so for those |
||
63 | * items, there are no subkeys. |
||
64 | */ |
||
65 | protected $data = []; |
||
66 | |||
67 | /** |
||
68 | * The persistent store object. An instance of LCStore. |
||
69 | * |
||
70 | * @var LCStore |
||
71 | */ |
||
72 | private $store; |
||
73 | |||
74 | /** |
||
75 | * A 2-d associative array, code/key, where presence indicates that the item |
||
76 | * is loaded. Value arbitrary. |
||
77 | * |
||
78 | * For split items, if set, this indicates that all of the subitems have been |
||
79 | * loaded. |
||
80 | */ |
||
81 | private $loadedItems = []; |
||
82 | |||
83 | /** |
||
84 | * A 3-d associative array, code/key/subkey, where presence indicates that |
||
85 | * the subitem is loaded. Only used for the split items, i.e. messages. |
||
86 | */ |
||
87 | private $loadedSubitems = []; |
||
88 | |||
89 | /** |
||
90 | * An array where presence of a key indicates that that language has been |
||
91 | * initialised. Initialisation includes checking for cache expiry and doing |
||
92 | * any necessary updates. |
||
93 | */ |
||
94 | private $initialisedLangs = []; |
||
95 | |||
96 | /** |
||
97 | * An array mapping non-existent pseudo-languages to fallback languages. This |
||
98 | * is filled by initShallowFallback() when data is requested from a language |
||
99 | * that lacks a Messages*.php file. |
||
100 | */ |
||
101 | private $shallowFallbacks = []; |
||
102 | |||
103 | /** |
||
104 | * An array where the keys are codes that have been recached by this instance. |
||
105 | */ |
||
106 | private $recachedLangs = []; |
||
107 | |||
108 | /** |
||
109 | * All item keys |
||
110 | */ |
||
111 | static public $allKeys = [ |
||
112 | 'fallback', 'namespaceNames', 'bookstoreList', |
||
113 | 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable', |
||
114 | 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension', |
||
115 | 'linkTrail', 'linkPrefixCharset', 'namespaceAliases', |
||
116 | 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap', |
||
117 | 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases', |
||
118 | 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases', |
||
119 | 'digitGroupingPattern', 'pluralRules', 'pluralRuleTypes', 'compiledPluralRules', |
||
120 | ]; |
||
121 | |||
122 | /** |
||
123 | * Keys for items which consist of associative arrays, which may be merged |
||
124 | * by a fallback sequence. |
||
125 | */ |
||
126 | static public $mergeableMapKeys = [ 'messages', 'namespaceNames', |
||
127 | 'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages' |
||
128 | ]; |
||
129 | |||
130 | /** |
||
131 | * Keys for items which are a numbered array. |
||
132 | */ |
||
133 | static public $mergeableListKeys = [ 'extraUserToggles' ]; |
||
134 | |||
135 | /** |
||
136 | * Keys for items which contain an array of arrays of equivalent aliases |
||
137 | * for each subitem. The aliases may be merged by a fallback sequence. |
||
138 | */ |
||
139 | static public $mergeableAliasListKeys = [ 'specialPageAliases' ]; |
||
140 | |||
141 | /** |
||
142 | * Keys for items which contain an associative array, and may be merged if |
||
143 | * the primary value contains the special array key "inherit". That array |
||
144 | * key is removed after the first merge. |
||
145 | */ |
||
146 | static public $optionalMergeKeys = [ 'bookstoreList' ]; |
||
147 | |||
148 | /** |
||
149 | * Keys for items that are formatted like $magicWords |
||
150 | */ |
||
151 | static public $magicWordKeys = [ 'magicWords' ]; |
||
152 | |||
153 | /** |
||
154 | * Keys for items where the subitems are stored in the backend separately. |
||
155 | */ |
||
156 | static public $splitKeys = [ 'messages' ]; |
||
157 | |||
158 | /** |
||
159 | * Keys which are loaded automatically by initLanguage() |
||
160 | */ |
||
161 | static public $preloadedKeys = [ 'dateFormats', 'namespaceNames' ]; |
||
162 | |||
163 | /** |
||
164 | * Associative array of cached plural rules. The key is the language code, |
||
165 | * the value is an array of plural rules for that language. |
||
166 | */ |
||
167 | private $pluralRules = null; |
||
168 | |||
169 | /** |
||
170 | * Associative array of cached plural rule types. The key is the language |
||
171 | * code, the value is an array of plural rule types for that language. For |
||
172 | * example, $pluralRuleTypes['ar'] = ['zero', 'one', 'two', 'few', 'many']. |
||
173 | * The index for each rule type matches the index for the rule in |
||
174 | * $pluralRules, thus allowing correlation between the two. The reason we |
||
175 | * don't just use the type names as the keys in $pluralRules is because |
||
176 | * Language::convertPlural applies the rules based on numeric order (or |
||
177 | * explicit numeric parameter), not based on the name of the rule type. For |
||
178 | * example, {{plural:count|wordform1|wordform2|wordform3}}, rather than |
||
179 | * {{plural:count|one=wordform1|two=wordform2|many=wordform3}}. |
||
180 | */ |
||
181 | private $pluralRuleTypes = null; |
||
182 | |||
183 | private $mergeableKeys = null; |
||
184 | |||
185 | /** |
||
186 | * Constructor. |
||
187 | * For constructor parameters, see the documentation in DefaultSettings.php |
||
188 | * for $wgLocalisationCacheConf. |
||
189 | * |
||
190 | * @param array $conf |
||
191 | * @throws MWException |
||
192 | */ |
||
193 | function __construct( $conf ) { |
||
194 | global $wgCacheDirectory; |
||
195 | |||
196 | $this->conf = $conf; |
||
197 | $storeConf = []; |
||
198 | if ( !empty( $conf['storeClass'] ) ) { |
||
199 | $storeClass = $conf['storeClass']; |
||
200 | } else { |
||
201 | switch ( $conf['store'] ) { |
||
202 | case 'files': |
||
203 | case 'file': |
||
204 | $storeClass = 'LCStoreCDB'; |
||
205 | break; |
||
206 | case 'db': |
||
207 | $storeClass = 'LCStoreDB'; |
||
208 | break; |
||
209 | case 'array': |
||
210 | $storeClass = 'LCStoreStaticArray'; |
||
211 | break; |
||
212 | case 'detect': |
||
213 | if ( !empty( $conf['storeDirectory'] ) ) { |
||
214 | $storeClass = 'LCStoreCDB'; |
||
215 | } else { |
||
216 | $cacheDir = $wgCacheDirectory ?: wfTempDir(); |
||
217 | if ( $cacheDir ) { |
||
218 | $storeConf['directory'] = $cacheDir; |
||
219 | $storeClass = 'LCStoreCDB'; |
||
220 | } else { |
||
221 | $storeClass = 'LCStoreDB'; |
||
222 | } |
||
223 | } |
||
224 | break; |
||
225 | default: |
||
226 | throw new MWException( |
||
227 | 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' ); |
||
228 | } |
||
229 | } |
||
230 | |||
231 | wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" ); |
||
232 | if ( !empty( $conf['storeDirectory'] ) ) { |
||
233 | $storeConf['directory'] = $conf['storeDirectory']; |
||
234 | } |
||
235 | |||
236 | $this->store = new $storeClass( $storeConf ); |
||
237 | foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) { |
||
238 | if ( isset( $conf[$var] ) ) { |
||
239 | $this->$var = $conf[$var]; |
||
240 | } |
||
241 | } |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * Returns true if the given key is mergeable, that is, if it is an associative |
||
246 | * array which can be merged through a fallback sequence. |
||
247 | * @param string $key |
||
248 | * @return bool |
||
249 | */ |
||
250 | public function isMergeableKey( $key ) { |
||
251 | if ( $this->mergeableKeys === null ) { |
||
252 | $this->mergeableKeys = array_flip( array_merge( |
||
253 | self::$mergeableMapKeys, |
||
254 | self::$mergeableListKeys, |
||
255 | self::$mergeableAliasListKeys, |
||
256 | self::$optionalMergeKeys, |
||
257 | self::$magicWordKeys |
||
258 | ) ); |
||
259 | } |
||
260 | |||
261 | return isset( $this->mergeableKeys[$key] ); |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Get a cache item. |
||
266 | * |
||
267 | * Warning: this may be slow for split items (messages), since it will |
||
268 | * need to fetch all of the subitems from the cache individually. |
||
269 | * @param string $code |
||
270 | * @param string $key |
||
271 | * @return mixed |
||
272 | */ |
||
273 | public function getItem( $code, $key ) { |
||
274 | if ( !isset( $this->loadedItems[$code][$key] ) ) { |
||
275 | $this->loadItem( $code, $key ); |
||
276 | } |
||
277 | |||
278 | if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) { |
||
279 | return $this->shallowFallbacks[$code]; |
||
280 | } |
||
281 | |||
282 | return $this->data[$code][$key]; |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * Get a subitem, for instance a single message for a given language. |
||
287 | * @param string $code |
||
288 | * @param string $key |
||
289 | * @param string $subkey |
||
290 | * @return mixed|null |
||
291 | */ |
||
292 | public function getSubitem( $code, $key, $subkey ) { |
||
293 | if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) && |
||
294 | !isset( $this->loadedItems[$code][$key] ) |
||
295 | ) { |
||
296 | $this->loadSubitem( $code, $key, $subkey ); |
||
297 | } |
||
298 | |||
299 | if ( isset( $this->data[$code][$key][$subkey] ) ) { |
||
300 | return $this->data[$code][$key][$subkey]; |
||
301 | } else { |
||
302 | return null; |
||
303 | } |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * Get the list of subitem keys for a given item. |
||
308 | * |
||
309 | * This is faster than array_keys($lc->getItem(...)) for the items listed in |
||
310 | * self::$splitKeys. |
||
311 | * |
||
312 | * Will return null if the item is not found, or false if the item is not an |
||
313 | * array. |
||
314 | * @param string $code |
||
315 | * @param string $key |
||
316 | * @return bool|null|string |
||
317 | */ |
||
318 | public function getSubitemList( $code, $key ) { |
||
319 | if ( in_array( $key, self::$splitKeys ) ) { |
||
320 | return $this->getSubitem( $code, 'list', $key ); |
||
321 | } else { |
||
322 | $item = $this->getItem( $code, $key ); |
||
323 | if ( is_array( $item ) ) { |
||
324 | return array_keys( $item ); |
||
325 | } else { |
||
326 | return false; |
||
327 | } |
||
328 | } |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Load an item into the cache. |
||
333 | * @param string $code |
||
334 | * @param string $key |
||
335 | */ |
||
336 | protected function loadItem( $code, $key ) { |
||
337 | if ( !isset( $this->initialisedLangs[$code] ) ) { |
||
338 | $this->initLanguage( $code ); |
||
339 | } |
||
340 | |||
341 | // Check to see if initLanguage() loaded it for us |
||
342 | if ( isset( $this->loadedItems[$code][$key] ) ) { |
||
343 | return; |
||
344 | } |
||
345 | |||
346 | if ( isset( $this->shallowFallbacks[$code] ) ) { |
||
347 | $this->loadItem( $this->shallowFallbacks[$code], $key ); |
||
348 | |||
349 | return; |
||
350 | } |
||
351 | |||
352 | if ( in_array( $key, self::$splitKeys ) ) { |
||
353 | $subkeyList = $this->getSubitem( $code, 'list', $key ); |
||
354 | foreach ( $subkeyList as $subkey ) { |
||
355 | if ( isset( $this->data[$code][$key][$subkey] ) ) { |
||
356 | continue; |
||
357 | } |
||
358 | $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey ); |
||
359 | } |
||
360 | } else { |
||
361 | $this->data[$code][$key] = $this->store->get( $code, $key ); |
||
362 | } |
||
363 | |||
364 | $this->loadedItems[$code][$key] = true; |
||
365 | } |
||
366 | |||
367 | /** |
||
368 | * Load a subitem into the cache |
||
369 | * @param string $code |
||
370 | * @param string $key |
||
371 | * @param string $subkey |
||
372 | */ |
||
373 | protected function loadSubitem( $code, $key, $subkey ) { |
||
374 | if ( !in_array( $key, self::$splitKeys ) ) { |
||
375 | $this->loadItem( $code, $key ); |
||
376 | |||
377 | return; |
||
378 | } |
||
379 | |||
380 | if ( !isset( $this->initialisedLangs[$code] ) ) { |
||
381 | $this->initLanguage( $code ); |
||
382 | } |
||
383 | |||
384 | // Check to see if initLanguage() loaded it for us |
||
385 | if ( isset( $this->loadedItems[$code][$key] ) || |
||
386 | isset( $this->loadedSubitems[$code][$key][$subkey] ) |
||
387 | ) { |
||
388 | return; |
||
389 | } |
||
390 | |||
391 | if ( isset( $this->shallowFallbacks[$code] ) ) { |
||
392 | $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey ); |
||
393 | |||
394 | return; |
||
395 | } |
||
396 | |||
397 | $value = $this->store->get( $code, "$key:$subkey" ); |
||
398 | $this->data[$code][$key][$subkey] = $value; |
||
399 | $this->loadedSubitems[$code][$key][$subkey] = true; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * Returns true if the cache identified by $code is missing or expired. |
||
404 | * |
||
405 | * @param string $code |
||
406 | * |
||
407 | * @return bool |
||
408 | */ |
||
409 | public function isExpired( $code ) { |
||
410 | if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) { |
||
411 | wfDebug( __METHOD__ . "($code): forced reload\n" ); |
||
412 | |||
413 | return true; |
||
414 | } |
||
415 | |||
416 | $deps = $this->store->get( $code, 'deps' ); |
||
417 | $keys = $this->store->get( $code, 'list' ); |
||
418 | $preload = $this->store->get( $code, 'preload' ); |
||
419 | // Different keys may expire separately for some stores |
||
420 | if ( $deps === null || $keys === null || $preload === null ) { |
||
421 | wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" ); |
||
422 | |||
423 | return true; |
||
424 | } |
||
425 | |||
426 | foreach ( $deps as $dep ) { |
||
427 | // Because we're unserializing stuff from cache, we |
||
428 | // could receive objects of classes that don't exist |
||
429 | // anymore (e.g. uninstalled extensions) |
||
430 | // When this happens, always expire the cache |
||
431 | if ( !$dep instanceof CacheDependency || $dep->isExpired() ) { |
||
432 | wfDebug( __METHOD__ . "($code): cache for $code expired due to " . |
||
433 | get_class( $dep ) . "\n" ); |
||
434 | |||
435 | return true; |
||
436 | } |
||
437 | } |
||
438 | |||
439 | return false; |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * Initialise a language in this object. Rebuild the cache if necessary. |
||
444 | * @param string $code |
||
445 | * @throws MWException |
||
446 | */ |
||
447 | protected function initLanguage( $code ) { |
||
448 | if ( isset( $this->initialisedLangs[$code] ) ) { |
||
449 | return; |
||
450 | } |
||
451 | |||
452 | $this->initialisedLangs[$code] = true; |
||
453 | |||
454 | # If the code is of the wrong form for a Messages*.php file, do a shallow fallback |
||
455 | if ( !Language::isValidBuiltInCode( $code ) ) { |
||
456 | $this->initShallowFallback( $code, 'en' ); |
||
457 | |||
458 | return; |
||
459 | } |
||
460 | |||
461 | # Recache the data if necessary |
||
462 | if ( !$this->manualRecache && $this->isExpired( $code ) ) { |
||
463 | if ( Language::isSupportedLanguage( $code ) ) { |
||
464 | $this->recache( $code ); |
||
465 | } elseif ( $code === 'en' ) { |
||
466 | throw new MWException( 'MessagesEn.php is missing.' ); |
||
467 | } else { |
||
468 | $this->initShallowFallback( $code, 'en' ); |
||
469 | } |
||
470 | |||
471 | return; |
||
472 | } |
||
473 | |||
474 | # Preload some stuff |
||
475 | $preload = $this->getItem( $code, 'preload' ); |
||
476 | if ( $preload === null ) { |
||
477 | if ( $this->manualRecache ) { |
||
478 | // No Messages*.php file. Do shallow fallback to en. |
||
479 | if ( $code === 'en' ) { |
||
480 | throw new MWException( 'No localisation cache found for English. ' . |
||
481 | 'Please run maintenance/rebuildLocalisationCache.php.' ); |
||
482 | } |
||
483 | $this->initShallowFallback( $code, 'en' ); |
||
484 | |||
485 | return; |
||
486 | } else { |
||
487 | throw new MWException( 'Invalid or missing localisation cache.' ); |
||
488 | } |
||
489 | } |
||
490 | $this->data[$code] = $preload; |
||
491 | foreach ( $preload as $key => $item ) { |
||
492 | if ( in_array( $key, self::$splitKeys ) ) { |
||
493 | foreach ( $item as $subkey => $subitem ) { |
||
494 | $this->loadedSubitems[$code][$key][$subkey] = true; |
||
495 | } |
||
496 | } else { |
||
497 | $this->loadedItems[$code][$key] = true; |
||
498 | } |
||
499 | } |
||
500 | } |
||
501 | |||
502 | /** |
||
503 | * Create a fallback from one language to another, without creating a |
||
504 | * complete persistent cache. |
||
505 | * @param string $primaryCode |
||
506 | * @param string $fallbackCode |
||
507 | */ |
||
508 | public function initShallowFallback( $primaryCode, $fallbackCode ) { |
||
509 | $this->data[$primaryCode] =& $this->data[$fallbackCode]; |
||
510 | $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode]; |
||
511 | $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode]; |
||
512 | $this->shallowFallbacks[$primaryCode] = $fallbackCode; |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * Read a PHP file containing localisation data. |
||
517 | * @param string $_fileName |
||
518 | * @param string $_fileType |
||
519 | * @throws MWException |
||
520 | * @return array |
||
521 | */ |
||
522 | protected function readPHPFile( $_fileName, $_fileType ) { |
||
523 | // Disable APC caching |
||
524 | MediaWiki\suppressWarnings(); |
||
525 | $_apcEnabled = ini_set( 'apc.cache_by_default', '0' ); |
||
526 | MediaWiki\restoreWarnings(); |
||
527 | |||
528 | include $_fileName; |
||
529 | |||
530 | MediaWiki\suppressWarnings(); |
||
531 | ini_set( 'apc.cache_by_default', $_apcEnabled ); |
||
532 | MediaWiki\restoreWarnings(); |
||
533 | |||
534 | if ( $_fileType == 'core' || $_fileType == 'extension' ) { |
||
535 | $data = compact( self::$allKeys ); |
||
536 | } elseif ( $_fileType == 'aliases' ) { |
||
537 | $data = compact( 'aliases' ); |
||
538 | } else { |
||
539 | throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" ); |
||
540 | } |
||
541 | |||
542 | return $data; |
||
543 | } |
||
544 | |||
545 | /** |
||
546 | * Read a JSON file containing localisation messages. |
||
547 | * @param string $fileName Name of file to read |
||
548 | * @throws MWException If there is a syntax error in the JSON file |
||
549 | * @return array Array with a 'messages' key, or empty array if the file doesn't exist |
||
550 | */ |
||
551 | public function readJSONFile( $fileName ) { |
||
552 | |||
553 | if ( !is_readable( $fileName ) ) { |
||
554 | return []; |
||
555 | } |
||
556 | |||
557 | $json = file_get_contents( $fileName ); |
||
558 | if ( $json === false ) { |
||
559 | return []; |
||
560 | } |
||
561 | |||
562 | $data = FormatJson::decode( $json, true ); |
||
563 | if ( $data === null ) { |
||
564 | |||
565 | throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" ); |
||
566 | } |
||
567 | |||
568 | // Remove keys starting with '@', they're reserved for metadata and non-message data |
||
569 | foreach ( $data as $key => $unused ) { |
||
570 | if ( $key === '' || $key[0] === '@' ) { |
||
571 | unset( $data[$key] ); |
||
572 | } |
||
573 | } |
||
574 | |||
575 | // The JSON format only supports messages, none of the other variables, so wrap the data |
||
576 | return [ 'messages' => $data ]; |
||
577 | } |
||
578 | |||
579 | /** |
||
580 | * Get the compiled plural rules for a given language from the XML files. |
||
581 | * @since 1.20 |
||
582 | * @param string $code |
||
583 | * @return array|null |
||
584 | */ |
||
585 | public function getCompiledPluralRules( $code ) { |
||
586 | $rules = $this->getPluralRules( $code ); |
||
587 | if ( $rules === null ) { |
||
588 | return null; |
||
589 | } |
||
590 | try { |
||
591 | $compiledRules = Evaluator::compile( $rules ); |
||
592 | } catch ( CLDRPluralRuleError $e ) { |
||
593 | wfDebugLog( 'l10n', $e->getMessage() ); |
||
594 | |||
595 | return []; |
||
596 | } |
||
597 | |||
598 | return $compiledRules; |
||
599 | } |
||
600 | |||
601 | /** |
||
602 | * Get the plural rules for a given language from the XML files. |
||
603 | * Cached. |
||
604 | * @since 1.20 |
||
605 | * @param string $code |
||
606 | * @return array|null |
||
607 | */ |
||
608 | View Code Duplication | public function getPluralRules( $code ) { |
|
609 | if ( $this->pluralRules === null ) { |
||
610 | $this->loadPluralFiles(); |
||
611 | } |
||
612 | if ( !isset( $this->pluralRules[$code] ) ) { |
||
613 | return null; |
||
614 | } else { |
||
615 | return $this->pluralRules[$code]; |
||
616 | } |
||
617 | } |
||
618 | |||
619 | /** |
||
620 | * Get the plural rule types for a given language from the XML files. |
||
621 | * Cached. |
||
622 | * @since 1.22 |
||
623 | * @param string $code |
||
624 | * @return array|null |
||
625 | */ |
||
626 | View Code Duplication | public function getPluralRuleTypes( $code ) { |
|
627 | if ( $this->pluralRuleTypes === null ) { |
||
628 | $this->loadPluralFiles(); |
||
629 | } |
||
630 | if ( !isset( $this->pluralRuleTypes[$code] ) ) { |
||
631 | return null; |
||
632 | } else { |
||
633 | return $this->pluralRuleTypes[$code]; |
||
634 | } |
||
635 | } |
||
636 | |||
637 | /** |
||
638 | * Load the plural XML files. |
||
639 | */ |
||
640 | protected function loadPluralFiles() { |
||
641 | global $IP; |
||
642 | $cldrPlural = "$IP/languages/data/plurals.xml"; |
||
643 | $mwPlural = "$IP/languages/data/plurals-mediawiki.xml"; |
||
644 | // Load CLDR plural rules |
||
645 | $this->loadPluralFile( $cldrPlural ); |
||
646 | if ( file_exists( $mwPlural ) ) { |
||
647 | // Override or extend |
||
648 | $this->loadPluralFile( $mwPlural ); |
||
649 | } |
||
650 | } |
||
651 | |||
652 | /** |
||
653 | * Load a plural XML file with the given filename, compile the relevant |
||
654 | * rules, and save the compiled rules in a process-local cache. |
||
655 | * |
||
656 | * @param string $fileName |
||
657 | * @throws MWException |
||
658 | */ |
||
659 | protected function loadPluralFile( $fileName ) { |
||
660 | // Use file_get_contents instead of DOMDocument::load (T58439) |
||
661 | $xml = file_get_contents( $fileName ); |
||
662 | if ( !$xml ) { |
||
663 | throw new MWException( "Unable to read plurals file $fileName" ); |
||
664 | } |
||
665 | $doc = new DOMDocument; |
||
666 | $doc->loadXML( $xml ); |
||
667 | $rulesets = $doc->getElementsByTagName( "pluralRules" ); |
||
668 | foreach ( $rulesets as $ruleset ) { |
||
669 | $codes = $ruleset->getAttribute( 'locales' ); |
||
670 | $rules = []; |
||
671 | $ruleTypes = []; |
||
672 | $ruleElements = $ruleset->getElementsByTagName( "pluralRule" ); |
||
673 | foreach ( $ruleElements as $elt ) { |
||
674 | $ruleType = $elt->getAttribute( 'count' ); |
||
675 | if ( $ruleType === 'other' ) { |
||
676 | // Don't record "other" rules, which have an empty condition |
||
677 | continue; |
||
678 | } |
||
679 | $rules[] = $elt->nodeValue; |
||
680 | $ruleTypes[] = $ruleType; |
||
681 | } |
||
682 | foreach ( explode( ' ', $codes ) as $code ) { |
||
683 | $this->pluralRules[$code] = $rules; |
||
684 | $this->pluralRuleTypes[$code] = $ruleTypes; |
||
685 | } |
||
686 | } |
||
687 | } |
||
688 | |||
689 | /** |
||
690 | * Read the data from the source files for a given language, and register |
||
691 | * the relevant dependencies in the $deps array. If the localisation |
||
692 | * exists, the data array is returned, otherwise false is returned. |
||
693 | * |
||
694 | * @param string $code |
||
695 | * @param array $deps |
||
696 | * @return array |
||
697 | */ |
||
698 | protected function readSourceFilesAndRegisterDeps( $code, &$deps ) { |
||
699 | global $IP; |
||
700 | |||
701 | // This reads in the PHP i18n file with non-messages l10n data |
||
702 | $fileName = Language::getMessagesFileName( $code ); |
||
703 | if ( !file_exists( $fileName ) ) { |
||
704 | $data = []; |
||
705 | } else { |
||
706 | $deps[] = new FileDependency( $fileName ); |
||
707 | $data = $this->readPHPFile( $fileName, 'core' ); |
||
708 | } |
||
709 | |||
710 | # Load CLDR plural rules for JavaScript |
||
711 | $data['pluralRules'] = $this->getPluralRules( $code ); |
||
712 | # And for PHP |
||
713 | $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code ); |
||
714 | # Load plural rule types |
||
715 | $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code ); |
||
716 | |||
717 | $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" ); |
||
718 | $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" ); |
||
719 | |||
720 | return $data; |
||
721 | } |
||
722 | |||
723 | /** |
||
724 | * Merge two localisation values, a primary and a fallback, overwriting the |
||
725 | * primary value in place. |
||
726 | * @param string $key |
||
727 | * @param mixed $value |
||
728 | * @param mixed $fallbackValue |
||
729 | */ |
||
730 | protected function mergeItem( $key, &$value, $fallbackValue ) { |
||
731 | if ( !is_null( $value ) ) { |
||
732 | if ( !is_null( $fallbackValue ) ) { |
||
733 | if ( in_array( $key, self::$mergeableMapKeys ) ) { |
||
734 | $value = $value + $fallbackValue; |
||
735 | } elseif ( in_array( $key, self::$mergeableListKeys ) ) { |
||
736 | $value = array_unique( array_merge( $fallbackValue, $value ) ); |
||
737 | } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) { |
||
738 | $value = array_merge_recursive( $value, $fallbackValue ); |
||
739 | } elseif ( in_array( $key, self::$optionalMergeKeys ) ) { |
||
740 | if ( !empty( $value['inherit'] ) ) { |
||
741 | $value = array_merge( $fallbackValue, $value ); |
||
742 | } |
||
743 | |||
744 | if ( isset( $value['inherit'] ) ) { |
||
745 | unset( $value['inherit'] ); |
||
746 | } |
||
747 | } elseif ( in_array( $key, self::$magicWordKeys ) ) { |
||
748 | $this->mergeMagicWords( $value, $fallbackValue ); |
||
749 | } |
||
750 | } |
||
751 | } else { |
||
752 | $value = $fallbackValue; |
||
753 | } |
||
754 | } |
||
755 | |||
756 | /** |
||
757 | * @param mixed $value |
||
758 | * @param mixed $fallbackValue |
||
759 | */ |
||
760 | protected function mergeMagicWords( &$value, $fallbackValue ) { |
||
761 | foreach ( $fallbackValue as $magicName => $fallbackInfo ) { |
||
762 | if ( !isset( $value[$magicName] ) ) { |
||
763 | $value[$magicName] = $fallbackInfo; |
||
764 | } else { |
||
765 | $oldSynonyms = array_slice( $fallbackInfo, 1 ); |
||
766 | $newSynonyms = array_slice( $value[$magicName], 1 ); |
||
767 | $synonyms = array_values( array_unique( array_merge( |
||
768 | $newSynonyms, $oldSynonyms ) ) ); |
||
769 | $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms ); |
||
770 | } |
||
771 | } |
||
772 | } |
||
773 | |||
774 | /** |
||
775 | * Given an array mapping language code to localisation value, such as is |
||
776 | * found in extension *.i18n.php files, iterate through a fallback sequence |
||
777 | * to merge the given data with an existing primary value. |
||
778 | * |
||
779 | * Returns true if any data from the extension array was used, false |
||
780 | * otherwise. |
||
781 | * @param array $codeSequence |
||
782 | * @param string $key |
||
783 | * @param mixed $value |
||
784 | * @param mixed $fallbackValue |
||
785 | * @return bool |
||
786 | */ |
||
787 | protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) { |
||
788 | $used = false; |
||
789 | foreach ( $codeSequence as $code ) { |
||
790 | if ( isset( $fallbackValue[$code] ) ) { |
||
791 | $this->mergeItem( $key, $value, $fallbackValue[$code] ); |
||
792 | $used = true; |
||
793 | } |
||
794 | } |
||
795 | |||
796 | return $used; |
||
797 | } |
||
798 | |||
799 | /** |
||
800 | * Gets the combined list of messages dirs from |
||
801 | * core and extensions |
||
802 | * |
||
803 | * @since 1.25 |
||
804 | * @return array |
||
805 | */ |
||
806 | public function getMessagesDirs() { |
||
807 | global $IP; |
||
808 | |||
809 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
||
810 | $messagesDirs = $config->get( 'MessagesDirs' ); |
||
811 | return [ |
||
812 | 'core' => "$IP/languages/i18n", |
||
813 | 'api' => "$IP/includes/api/i18n", |
||
814 | 'oojs-ui' => "$IP/resources/lib/oojs-ui/i18n", |
||
815 | ] + $messagesDirs; |
||
816 | } |
||
817 | |||
818 | /** |
||
819 | * Load localisation data for a given language for both core and extensions |
||
820 | * and save it to the persistent cache store and the process cache |
||
821 | * @param string $code |
||
822 | * @throws MWException |
||
823 | */ |
||
824 | public function recache( $code ) { |
||
825 | global $wgExtensionMessagesFiles; |
||
826 | |||
827 | if ( !$code ) { |
||
828 | throw new MWException( "Invalid language code requested" ); |
||
829 | } |
||
830 | $this->recachedLangs[$code] = true; |
||
831 | |||
832 | # Initial values |
||
833 | $initialData = array_fill_keys( self::$allKeys, null ); |
||
834 | $coreData = $initialData; |
||
835 | $deps = []; |
||
836 | |||
837 | # Load the primary localisation from the source file |
||
838 | $data = $this->readSourceFilesAndRegisterDeps( $code, $deps ); |
||
839 | if ( $data === false ) { |
||
840 | wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" ); |
||
841 | $coreData['fallback'] = 'en'; |
||
842 | } else { |
||
843 | wfDebug( __METHOD__ . ": got localisation for $code from source\n" ); |
||
844 | |||
845 | # Merge primary localisation |
||
846 | foreach ( $data as $key => $value ) { |
||
847 | $this->mergeItem( $key, $coreData[$key], $value ); |
||
848 | } |
||
849 | } |
||
850 | |||
851 | # Fill in the fallback if it's not there already |
||
852 | if ( is_null( $coreData['fallback'] ) ) { |
||
853 | $coreData['fallback'] = $code === 'en' ? false : 'en'; |
||
854 | } |
||
855 | if ( $coreData['fallback'] === false ) { |
||
856 | $coreData['fallbackSequence'] = []; |
||
857 | } else { |
||
858 | $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) ); |
||
859 | $len = count( $coreData['fallbackSequence'] ); |
||
860 | |||
861 | # Ensure that the sequence ends at en |
||
862 | if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) { |
||
863 | $coreData['fallbackSequence'][] = 'en'; |
||
864 | } |
||
865 | } |
||
866 | |||
867 | $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] ); |
||
868 | $messageDirs = $this->getMessagesDirs(); |
||
869 | |||
870 | # Load non-JSON localisation data for extensions |
||
871 | $extensionData = array_fill_keys( $codeSequence, $initialData ); |
||
872 | foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) { |
||
873 | if ( isset( $messageDirs[$extension] ) ) { |
||
874 | # This extension has JSON message data; skip the PHP shim |
||
875 | continue; |
||
876 | } |
||
877 | |||
878 | $data = $this->readPHPFile( $fileName, 'extension' ); |
||
879 | $used = false; |
||
880 | |||
881 | foreach ( $data as $key => $item ) { |
||
882 | foreach ( $codeSequence as $csCode ) { |
||
883 | View Code Duplication | if ( isset( $item[$csCode] ) ) { |
|
884 | $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] ); |
||
885 | $used = true; |
||
886 | } |
||
887 | } |
||
888 | } |
||
889 | |||
890 | if ( $used ) { |
||
891 | $deps[] = new FileDependency( $fileName ); |
||
892 | } |
||
893 | } |
||
894 | |||
895 | # Load the localisation data for each fallback, then merge it into the full array |
||
896 | $allData = $initialData; |
||
897 | foreach ( $codeSequence as $csCode ) { |
||
898 | $csData = $initialData; |
||
899 | |||
900 | # Load core messages and the extension localisations. |
||
901 | foreach ( $messageDirs as $dirs ) { |
||
902 | foreach ( (array)$dirs as $dir ) { |
||
903 | $fileName = "$dir/$csCode.json"; |
||
904 | $data = $this->readJSONFile( $fileName ); |
||
905 | |||
906 | foreach ( $data as $key => $item ) { |
||
907 | $this->mergeItem( $key, $csData[$key], $item ); |
||
908 | } |
||
909 | |||
910 | $deps[] = new FileDependency( $fileName ); |
||
911 | } |
||
912 | } |
||
913 | |||
914 | # Merge non-JSON extension data |
||
915 | View Code Duplication | if ( isset( $extensionData[$csCode] ) ) { |
|
916 | foreach ( $extensionData[$csCode] as $key => $item ) { |
||
917 | $this->mergeItem( $key, $csData[$key], $item ); |
||
918 | } |
||
919 | } |
||
920 | |||
921 | if ( $csCode === $code ) { |
||
922 | # Merge core data into extension data |
||
923 | foreach ( $coreData as $key => $item ) { |
||
924 | $this->mergeItem( $key, $csData[$key], $item ); |
||
925 | } |
||
926 | View Code Duplication | } else { |
|
927 | # Load the secondary localisation from the source file to |
||
928 | # avoid infinite cycles on cyclic fallbacks |
||
929 | $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps ); |
||
930 | if ( $fbData !== false ) { |
||
931 | # Only merge the keys that make sense to merge |
||
932 | foreach ( self::$allKeys as $key ) { |
||
933 | if ( !isset( $fbData[$key] ) ) { |
||
934 | continue; |
||
935 | } |
||
936 | |||
937 | if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { |
||
938 | $this->mergeItem( $key, $csData[$key], $fbData[$key] ); |
||
939 | } |
||
940 | } |
||
941 | } |
||
942 | } |
||
943 | |||
944 | # Allow extensions an opportunity to adjust the data for this |
||
945 | # fallback |
||
946 | Hooks::run( 'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] ); |
||
947 | |||
948 | # Merge the data for this fallback into the final array |
||
949 | View Code Duplication | if ( $csCode === $code ) { |
|
950 | $allData = $csData; |
||
951 | } else { |
||
952 | foreach ( self::$allKeys as $key ) { |
||
953 | if ( !isset( $csData[$key] ) ) { |
||
954 | continue; |
||
955 | } |
||
956 | |||
957 | if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) { |
||
958 | $this->mergeItem( $key, $allData[$key], $csData[$key] ); |
||
959 | } |
||
960 | } |
||
961 | } |
||
962 | } |
||
963 | |||
964 | # Add cache dependencies for any referenced globals |
||
965 | $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' ); |
||
966 | // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs(). |
||
967 | // We use the key 'wgMessagesDirs' for historical reasons. |
||
968 | $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' ); |
||
969 | $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' ); |
||
970 | |||
971 | # Add dependencies to the cache entry |
||
972 | $allData['deps'] = $deps; |
||
973 | |||
974 | # Replace spaces with underscores in namespace names |
||
975 | $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] ); |
||
976 | |||
977 | # And do the same for special page aliases. $page is an array. |
||
978 | foreach ( $allData['specialPageAliases'] as &$page ) { |
||
979 | $page = str_replace( ' ', '_', $page ); |
||
980 | } |
||
981 | # Decouple the reference to prevent accidental damage |
||
982 | unset( $page ); |
||
983 | |||
984 | # If there were no plural rules, return an empty array |
||
985 | if ( $allData['pluralRules'] === null ) { |
||
986 | $allData['pluralRules'] = []; |
||
987 | } |
||
988 | if ( $allData['compiledPluralRules'] === null ) { |
||
989 | $allData['compiledPluralRules'] = []; |
||
990 | } |
||
991 | # If there were no plural rule types, return an empty array |
||
992 | if ( $allData['pluralRuleTypes'] === null ) { |
||
993 | $allData['pluralRuleTypes'] = []; |
||
994 | } |
||
995 | |||
996 | # Set the list keys |
||
997 | $allData['list'] = []; |
||
998 | foreach ( self::$splitKeys as $key ) { |
||
999 | $allData['list'][$key] = array_keys( $allData[$key] ); |
||
1000 | } |
||
1001 | # Run hooks |
||
1002 | $purgeBlobs = true; |
||
1003 | Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$purgeBlobs ] ); |
||
1004 | |||
1005 | if ( is_null( $allData['namespaceNames'] ) ) { |
||
1006 | throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' . |
||
1007 | 'Check that your languages/messages/MessagesEn.php file is intact.' ); |
||
1008 | } |
||
1009 | |||
1010 | # Set the preload key |
||
1011 | $allData['preload'] = $this->buildPreload( $allData ); |
||
1012 | |||
1013 | # Save to the process cache and register the items loaded |
||
1014 | $this->data[$code] = $allData; |
||
1015 | foreach ( $allData as $key => $item ) { |
||
1016 | $this->loadedItems[$code][$key] = true; |
||
1017 | } |
||
1018 | |||
1019 | # Save to the persistent cache |
||
1020 | $this->store->startWrite( $code ); |
||
1021 | foreach ( $allData as $key => $value ) { |
||
1022 | if ( in_array( $key, self::$splitKeys ) ) { |
||
1023 | foreach ( $value as $subkey => $subvalue ) { |
||
1024 | $this->store->set( "$key:$subkey", $subvalue ); |
||
1025 | } |
||
1026 | } else { |
||
1027 | $this->store->set( $key, $value ); |
||
1028 | } |
||
1029 | } |
||
1030 | $this->store->finishWrite(); |
||
1031 | |||
1032 | # Clear out the MessageBlobStore |
||
1033 | # HACK: If using a null (i.e. disabled) storage backend, we |
||
1034 | # can't write to the MessageBlobStore either |
||
1035 | if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) { |
||
1036 | $blobStore = new MessageBlobStore(); |
||
1037 | $blobStore->clear(); |
||
1038 | } |
||
1039 | |||
1040 | } |
||
1041 | |||
1042 | /** |
||
1043 | * Build the preload item from the given pre-cache data. |
||
1044 | * |
||
1045 | * The preload item will be loaded automatically, improving performance |
||
1046 | * for the commonly-requested items it contains. |
||
1047 | * @param array $data |
||
1048 | * @return array |
||
1049 | */ |
||
1050 | protected function buildPreload( $data ) { |
||
1051 | $preload = [ 'messages' => [] ]; |
||
1052 | foreach ( self::$preloadedKeys as $key ) { |
||
1053 | $preload[$key] = $data[$key]; |
||
1054 | } |
||
1055 | |||
1056 | foreach ( $data['preloadedMessages'] as $subkey ) { |
||
1057 | if ( isset( $data['messages'][$subkey] ) ) { |
||
1058 | $subitem = $data['messages'][$subkey]; |
||
1059 | } else { |
||
1060 | $subitem = null; |
||
1061 | } |
||
1062 | $preload['messages'][$subkey] = $subitem; |
||
1063 | } |
||
1064 | |||
1065 | return $preload; |
||
1066 | } |
||
1067 | |||
1068 | /** |
||
1069 | * Unload the data for a given language from the object cache. |
||
1070 | * Reduces memory usage. |
||
1071 | * @param string $code |
||
1072 | */ |
||
1073 | public function unload( $code ) { |
||
1074 | unset( $this->data[$code] ); |
||
1075 | unset( $this->loadedItems[$code] ); |
||
1076 | unset( $this->loadedSubitems[$code] ); |
||
1077 | unset( $this->initialisedLangs[$code] ); |
||
1078 | unset( $this->shallowFallbacks[$code] ); |
||
1079 | |||
1080 | foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) { |
||
1081 | if ( $fbCode === $code ) { |
||
1082 | $this->unload( $shallowCode ); |
||
1083 | } |
||
1084 | } |
||
1085 | } |
||
1086 | |||
1087 | /** |
||
1088 | * Unload all data |
||
1089 | */ |
||
1090 | public function unloadAll() { |
||
1091 | foreach ( $this->initialisedLangs as $lang => $unused ) { |
||
1092 | $this->unload( $lang ); |
||
1093 | } |
||
1094 | } |
||
1095 | |||
1096 | /** |
||
1097 | * Disable the storage backend |
||
1098 | */ |
||
1099 | public function disableBackend() { |
||
1100 | $this->store = new LCStoreNull; |
||
1101 | $this->manualRecache = false; |
||
1102 | } |
||
1103 | |||
1104 | } |
||
1105 |
Let’s assume that you have a directory layout like this:
and let’s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: