Merge "(bug 37182) Removed hard coded parentheses in SpecialStatistics.php"
[mediawiki.git] / includes / LocalisationCache.php
blob9ce26d000be567dd63f3dab3bc543725774dd276
1 <?php
2 /**
3 * Cache of the contents of localisation files.
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.
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.
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
20 * @file
23 define( 'MW_LC_VERSION', 2 );
25 /**
26 * Class for caching the contents of localisation files, Messages*.php
27 * and *.i18n.php.
29 * An instance of this class is available using Language::getLocalisationCache().
31 * The values retrieved from here are merged, containing items from extension
32 * files, core messages files and the language fallback sequence (e.g. zh-cn ->
33 * zh-hans -> en ). Some common errors are corrected, for example namespace
34 * names with spaces instead of underscores, but heavyweight processing, such
35 * as grammatical transformation, is done by the caller.
37 class LocalisationCache {
38 /** Configuration associative array */
39 var $conf;
41 /**
42 * True if recaching should only be done on an explicit call to recache().
43 * Setting this reduces the overhead of cache freshness checking, which
44 * requires doing a stat() for every extension i18n file.
46 var $manualRecache = false;
48 /**
49 * True to treat all files as expired until they are regenerated by this object.
51 var $forceRecache = false;
53 /**
54 * The cache data. 3-d array, where the first key is the language code,
55 * the second key is the item key e.g. 'messages', and the third key is
56 * an item specific subkey index. Some items are not arrays and so for those
57 * items, there are no subkeys.
59 var $data = array();
61 /**
62 * The persistent store object. An instance of LCStore.
64 * @var LCStore
66 var $store;
68 /**
69 * A 2-d associative array, code/key, where presence indicates that the item
70 * is loaded. Value arbitrary.
72 * For split items, if set, this indicates that all of the subitems have been
73 * loaded.
75 var $loadedItems = array();
77 /**
78 * A 3-d associative array, code/key/subkey, where presence indicates that
79 * the subitem is loaded. Only used for the split items, i.e. messages.
81 var $loadedSubitems = array();
83 /**
84 * An array where presence of a key indicates that that language has been
85 * initialised. Initialisation includes checking for cache expiry and doing
86 * any necessary updates.
88 var $initialisedLangs = array();
90 /**
91 * An array mapping non-existent pseudo-languages to fallback languages. This
92 * is filled by initShallowFallback() when data is requested from a language
93 * that lacks a Messages*.php file.
95 var $shallowFallbacks = array();
97 /**
98 * An array where the keys are codes that have been recached by this instance.
100 var $recachedLangs = array();
103 * All item keys
105 static public $allKeys = array(
106 'fallback', 'namespaceNames', 'bookstoreList',
107 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
108 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
109 'linkTrail', 'namespaceAliases',
110 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
111 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
112 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
113 'digitGroupingPattern'
117 * Keys for items which consist of associative arrays, which may be merged
118 * by a fallback sequence.
120 static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
121 'dateFormats', 'imageFiles', 'preloadedMessages',
125 * Keys for items which are a numbered array.
127 static public $mergeableListKeys = array( 'extraUserToggles' );
130 * Keys for items which contain an array of arrays of equivalent aliases
131 * for each subitem. The aliases may be merged by a fallback sequence.
133 static public $mergeableAliasListKeys = array( 'specialPageAliases' );
136 * Keys for items which contain an associative array, and may be merged if
137 * the primary value contains the special array key "inherit". That array
138 * key is removed after the first merge.
140 static public $optionalMergeKeys = array( 'bookstoreList' );
143 * Keys for items that are formatted like $magicWords
145 static public $magicWordKeys = array( 'magicWords' );
148 * Keys for items where the subitems are stored in the backend separately.
150 static public $splitKeys = array( 'messages' );
153 * Keys which are loaded automatically by initLanguage()
155 static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
157 var $mergeableKeys = null;
160 * Constructor.
161 * For constructor parameters, see the documentation in DefaultSettings.php
162 * for $wgLocalisationCacheConf.
164 * @param $conf Array
166 function __construct( $conf ) {
167 global $wgCacheDirectory;
169 $this->conf = $conf;
170 $storeConf = array();
171 if ( !empty( $conf['storeClass'] ) ) {
172 $storeClass = $conf['storeClass'];
173 } else {
174 switch ( $conf['store'] ) {
175 case 'files':
176 case 'file':
177 $storeClass = 'LCStore_CDB';
178 break;
179 case 'db':
180 $storeClass = 'LCStore_DB';
181 break;
182 case 'accel':
183 $storeClass = 'LCStore_Accel';
184 break;
185 case 'detect':
186 $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
187 break;
188 default:
189 throw new MWException(
190 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
194 wfDebug( get_class( $this ) . ": using store $storeClass\n" );
195 if ( !empty( $conf['storeDirectory'] ) ) {
196 $storeConf['directory'] = $conf['storeDirectory'];
199 $this->store = new $storeClass( $storeConf );
200 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
201 if ( isset( $conf[$var] ) ) {
202 $this->$var = $conf[$var];
208 * Returns true if the given key is mergeable, that is, if it is an associative
209 * array which can be merged through a fallback sequence.
210 * @param $key
211 * @return bool
213 public function isMergeableKey( $key ) {
214 if ( $this->mergeableKeys === null ) {
215 $this->mergeableKeys = array_flip( array_merge(
216 self::$mergeableMapKeys,
217 self::$mergeableListKeys,
218 self::$mergeableAliasListKeys,
219 self::$optionalMergeKeys,
220 self::$magicWordKeys
221 ) );
223 return isset( $this->mergeableKeys[$key] );
227 * Get a cache item.
229 * Warning: this may be slow for split items (messages), since it will
230 * need to fetch all of the subitems from the cache individually.
231 * @param $code
232 * @param $key
233 * @return mixed
235 public function getItem( $code, $key ) {
236 if ( !isset( $this->loadedItems[$code][$key] ) ) {
237 wfProfileIn( __METHOD__.'-load' );
238 $this->loadItem( $code, $key );
239 wfProfileOut( __METHOD__.'-load' );
242 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
243 return $this->shallowFallbacks[$code];
246 return $this->data[$code][$key];
250 * Get a subitem, for instance a single message for a given language.
251 * @param $code
252 * @param $key
253 * @param $subkey
254 * @return null
256 public function getSubitem( $code, $key, $subkey ) {
257 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
258 !isset( $this->loadedItems[$code][$key] ) ) {
259 wfProfileIn( __METHOD__.'-load' );
260 $this->loadSubitem( $code, $key, $subkey );
261 wfProfileOut( __METHOD__.'-load' );
264 if ( isset( $this->data[$code][$key][$subkey] ) ) {
265 return $this->data[$code][$key][$subkey];
266 } else {
267 return null;
272 * Get the list of subitem keys for a given item.
274 * This is faster than array_keys($lc->getItem(...)) for the items listed in
275 * self::$splitKeys.
277 * Will return null if the item is not found, or false if the item is not an
278 * array.
279 * @param $code
280 * @param $key
281 * @return bool|null|string
283 public function getSubitemList( $code, $key ) {
284 if ( in_array( $key, self::$splitKeys ) ) {
285 return $this->getSubitem( $code, 'list', $key );
286 } else {
287 $item = $this->getItem( $code, $key );
288 if ( is_array( $item ) ) {
289 return array_keys( $item );
290 } else {
291 return false;
297 * Load an item into the cache.
298 * @param $code
299 * @param $key
301 protected function loadItem( $code, $key ) {
302 if ( !isset( $this->initialisedLangs[$code] ) ) {
303 $this->initLanguage( $code );
306 // Check to see if initLanguage() loaded it for us
307 if ( isset( $this->loadedItems[$code][$key] ) ) {
308 return;
311 if ( isset( $this->shallowFallbacks[$code] ) ) {
312 $this->loadItem( $this->shallowFallbacks[$code], $key );
313 return;
316 if ( in_array( $key, self::$splitKeys ) ) {
317 $subkeyList = $this->getSubitem( $code, 'list', $key );
318 foreach ( $subkeyList as $subkey ) {
319 if ( isset( $this->data[$code][$key][$subkey] ) ) {
320 continue;
322 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
324 } else {
325 $this->data[$code][$key] = $this->store->get( $code, $key );
328 $this->loadedItems[$code][$key] = true;
332 * Load a subitem into the cache
333 * @param $code
334 * @param $key
335 * @param $subkey
336 * @return
338 protected function loadSubitem( $code, $key, $subkey ) {
339 if ( !in_array( $key, self::$splitKeys ) ) {
340 $this->loadItem( $code, $key );
341 return;
344 if ( !isset( $this->initialisedLangs[$code] ) ) {
345 $this->initLanguage( $code );
348 // Check to see if initLanguage() loaded it for us
349 if ( isset( $this->loadedItems[$code][$key] ) ||
350 isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
351 return;
354 if ( isset( $this->shallowFallbacks[$code] ) ) {
355 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
356 return;
359 $value = $this->store->get( $code, "$key:$subkey" );
360 $this->data[$code][$key][$subkey] = $value;
361 $this->loadedSubitems[$code][$key][$subkey] = true;
365 * Returns true if the cache identified by $code is missing or expired.
366 * @return bool
368 public function isExpired( $code ) {
369 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
370 wfDebug( __METHOD__."($code): forced reload\n" );
371 return true;
374 $deps = $this->store->get( $code, 'deps' );
375 $keys = $this->store->get( $code, 'list', 'messages' );
376 $preload = $this->store->get( $code, 'preload' );
377 // Different keys may expire separately, at least in LCStore_Accel
378 if ( $deps === null || $keys === null || $preload === null ) {
379 wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
380 return true;
383 foreach ( $deps as $dep ) {
384 // Because we're unserializing stuff from cache, we
385 // could receive objects of classes that don't exist
386 // anymore (e.g. uninstalled extensions)
387 // When this happens, always expire the cache
388 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
389 wfDebug( __METHOD__."($code): cache for $code expired due to " .
390 get_class( $dep ) . "\n" );
391 return true;
395 return false;
399 * Initialise a language in this object. Rebuild the cache if necessary.
400 * @param $code
402 protected function initLanguage( $code ) {
403 if ( isset( $this->initialisedLangs[$code] ) ) {
404 return;
407 $this->initialisedLangs[$code] = true;
409 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
410 if ( !Language::isValidBuiltInCode( $code ) ) {
411 $this->initShallowFallback( $code, 'en' );
412 return;
415 # Recache the data if necessary
416 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
417 if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
418 $this->recache( $code );
419 } elseif ( $code === 'en' ) {
420 throw new MWException( 'MessagesEn.php is missing.' );
421 } else {
422 $this->initShallowFallback( $code, 'en' );
424 return;
427 # Preload some stuff
428 $preload = $this->getItem( $code, 'preload' );
429 if ( $preload === null ) {
430 if ( $this->manualRecache ) {
431 // No Messages*.php file. Do shallow fallback to en.
432 if ( $code === 'en' ) {
433 throw new MWException( 'No localisation cache found for English. ' .
434 'Please run maintenance/rebuildLocalisationCache.php.' );
436 $this->initShallowFallback( $code, 'en' );
437 return;
438 } else {
439 throw new MWException( 'Invalid or missing localisation cache.' );
442 $this->data[$code] = $preload;
443 foreach ( $preload as $key => $item ) {
444 if ( in_array( $key, self::$splitKeys ) ) {
445 foreach ( $item as $subkey => $subitem ) {
446 $this->loadedSubitems[$code][$key][$subkey] = true;
448 } else {
449 $this->loadedItems[$code][$key] = true;
455 * Create a fallback from one language to another, without creating a
456 * complete persistent cache.
457 * @param $primaryCode
458 * @param $fallbackCode
460 public function initShallowFallback( $primaryCode, $fallbackCode ) {
461 $this->data[$primaryCode] =& $this->data[$fallbackCode];
462 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
463 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
464 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
468 * Read a PHP file containing localisation data.
469 * @param $_fileName
470 * @param $_fileType
471 * @return array
473 protected function readPHPFile( $_fileName, $_fileType ) {
474 // Disable APC caching
475 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
476 include( $_fileName );
477 ini_set( 'apc.cache_by_default', $_apcEnabled );
479 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
480 $data = compact( self::$allKeys );
481 } elseif ( $_fileType == 'aliases' ) {
482 $data = compact( 'aliases' );
483 } else {
484 throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
487 return $data;
491 * Merge two localisation values, a primary and a fallback, overwriting the
492 * primary value in place.
493 * @param $key
494 * @param $value
495 * @param $fallbackValue
497 protected function mergeItem( $key, &$value, $fallbackValue ) {
498 if ( !is_null( $value ) ) {
499 if ( !is_null( $fallbackValue ) ) {
500 if ( in_array( $key, self::$mergeableMapKeys ) ) {
501 $value = $value + $fallbackValue;
502 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
503 $value = array_unique( array_merge( $fallbackValue, $value ) );
504 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
505 $value = array_merge_recursive( $value, $fallbackValue );
506 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
507 if ( !empty( $value['inherit'] ) ) {
508 $value = array_merge( $fallbackValue, $value );
511 if ( isset( $value['inherit'] ) ) {
512 unset( $value['inherit'] );
514 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
515 $this->mergeMagicWords( $value, $fallbackValue );
518 } else {
519 $value = $fallbackValue;
524 * @param $value
525 * @param $fallbackValue
527 protected function mergeMagicWords( &$value, $fallbackValue ) {
528 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
529 if ( !isset( $value[$magicName] ) ) {
530 $value[$magicName] = $fallbackInfo;
531 } else {
532 $oldSynonyms = array_slice( $fallbackInfo, 1 );
533 $newSynonyms = array_slice( $value[$magicName], 1 );
534 $synonyms = array_values( array_unique( array_merge(
535 $newSynonyms, $oldSynonyms ) ) );
536 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
542 * Given an array mapping language code to localisation value, such as is
543 * found in extension *.i18n.php files, iterate through a fallback sequence
544 * to merge the given data with an existing primary value.
546 * Returns true if any data from the extension array was used, false
547 * otherwise.
548 * @param $codeSequence
549 * @param $key
550 * @param $value
551 * @param $fallbackValue
552 * @return bool
554 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
555 $used = false;
556 foreach ( $codeSequence as $code ) {
557 if ( isset( $fallbackValue[$code] ) ) {
558 $this->mergeItem( $key, $value, $fallbackValue[$code] );
559 $used = true;
563 return $used;
567 * Load localisation data for a given language for both core and extensions
568 * and save it to the persistent cache store and the process cache
569 * @param $code
571 public function recache( $code ) {
572 global $wgExtensionMessagesFiles;
573 wfProfileIn( __METHOD__ );
575 if ( !$code ) {
576 throw new MWException( "Invalid language code requested" );
578 $this->recachedLangs[$code] = true;
580 # Initial values
581 $initialData = array_combine(
582 self::$allKeys,
583 array_fill( 0, count( self::$allKeys ), null ) );
584 $coreData = $initialData;
585 $deps = array();
587 # Load the primary localisation from the source file
588 $fileName = Language::getMessagesFileName( $code );
589 if ( !file_exists( $fileName ) ) {
590 wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
591 $coreData['fallback'] = 'en';
592 } else {
593 $deps[] = new FileDependency( $fileName );
594 $data = $this->readPHPFile( $fileName, 'core' );
595 wfDebug( __METHOD__.": got localisation for $code from source\n" );
597 # Merge primary localisation
598 foreach ( $data as $key => $value ) {
599 $this->mergeItem( $key, $coreData[$key], $value );
604 # Fill in the fallback if it's not there already
605 if ( is_null( $coreData['fallback'] ) ) {
606 $coreData['fallback'] = $code === 'en' ? false : 'en';
609 if ( $coreData['fallback'] === false ) {
610 $coreData['fallbackSequence'] = array();
611 } else {
612 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
613 $len = count( $coreData['fallbackSequence'] );
615 # Ensure that the sequence ends at en
616 if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
617 $coreData['fallbackSequence'][] = 'en';
620 # Load the fallback localisation item by item and merge it
621 foreach ( $coreData['fallbackSequence'] as $fbCode ) {
622 # Load the secondary localisation from the source file to
623 # avoid infinite cycles on cyclic fallbacks
624 $fbFilename = Language::getMessagesFileName( $fbCode );
626 if ( !file_exists( $fbFilename ) ) {
627 continue;
630 $deps[] = new FileDependency( $fbFilename );
631 $fbData = $this->readPHPFile( $fbFilename, 'core' );
633 foreach ( self::$allKeys as $key ) {
634 if ( !isset( $fbData[$key] ) ) {
635 continue;
638 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
639 $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
645 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
647 # Load the extension localisations
648 # This is done after the core because we know the fallback sequence now.
649 # But it has a higher precedence for merging so that we can support things
650 # like site-specific message overrides.
651 $allData = $initialData;
652 foreach ( $wgExtensionMessagesFiles as $fileName ) {
653 $data = $this->readPHPFile( $fileName, 'extension' );
654 $used = false;
656 foreach ( $data as $key => $item ) {
657 if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
658 $used = true;
662 if ( $used ) {
663 $deps[] = new FileDependency( $fileName );
667 # Merge core data into extension data
668 foreach ( $coreData as $key => $item ) {
669 $this->mergeItem( $key, $allData[$key], $item );
672 # Add cache dependencies for any referenced globals
673 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
674 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
676 # Add dependencies to the cache entry
677 $allData['deps'] = $deps;
679 # Replace spaces with underscores in namespace names
680 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
682 # And do the same for special page aliases. $page is an array.
683 foreach ( $allData['specialPageAliases'] as &$page ) {
684 $page = str_replace( ' ', '_', $page );
686 # Decouple the reference to prevent accidental damage
687 unset($page);
689 # Set the list keys
690 $allData['list'] = array();
691 foreach ( self::$splitKeys as $key ) {
692 $allData['list'][$key] = array_keys( $allData[$key] );
695 # Run hooks
696 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
698 if ( is_null( $allData['namespaceNames'] ) ) {
699 throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
700 'Check that your languages/messages/MessagesEn.php file is intact.' );
703 # Set the preload key
704 $allData['preload'] = $this->buildPreload( $allData );
706 # Save to the process cache and register the items loaded
707 $this->data[$code] = $allData;
708 foreach ( $allData as $key => $item ) {
709 $this->loadedItems[$code][$key] = true;
712 # Save to the persistent cache
713 $this->store->startWrite( $code );
714 foreach ( $allData as $key => $value ) {
715 if ( in_array( $key, self::$splitKeys ) ) {
716 foreach ( $value as $subkey => $subvalue ) {
717 $this->store->set( "$key:$subkey", $subvalue );
719 } else {
720 $this->store->set( $key, $value );
723 $this->store->finishWrite();
725 # Clear out the MessageBlobStore
726 # HACK: If using a null (i.e. disabled) storage backend, we
727 # can't write to the MessageBlobStore either
728 if ( !$this->store instanceof LCStore_Null ) {
729 MessageBlobStore::clear();
732 wfProfileOut( __METHOD__ );
736 * Build the preload item from the given pre-cache data.
738 * The preload item will be loaded automatically, improving performance
739 * for the commonly-requested items it contains.
740 * @param $data
741 * @return array
743 protected function buildPreload( $data ) {
744 $preload = array( 'messages' => array() );
745 foreach ( self::$preloadedKeys as $key ) {
746 $preload[$key] = $data[$key];
749 foreach ( $data['preloadedMessages'] as $subkey ) {
750 if ( isset( $data['messages'][$subkey] ) ) {
751 $subitem = $data['messages'][$subkey];
752 } else {
753 $subitem = null;
755 $preload['messages'][$subkey] = $subitem;
758 return $preload;
762 * Unload the data for a given language from the object cache.
763 * Reduces memory usage.
764 * @param $code
766 public function unload( $code ) {
767 unset( $this->data[$code] );
768 unset( $this->loadedItems[$code] );
769 unset( $this->loadedSubitems[$code] );
770 unset( $this->initialisedLangs[$code] );
772 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
773 if ( $fbCode === $code ) {
774 $this->unload( $shallowCode );
780 * Unload all data
782 public function unloadAll() {
783 foreach ( $this->initialisedLangs as $lang => $unused ) {
784 $this->unload( $lang );
789 * Disable the storage backend
791 public function disableBackend() {
792 $this->store = new LCStore_Null;
793 $this->manualRecache = false;
798 * Interface for the persistence layer of LocalisationCache.
800 * The persistence layer is two-level hierarchical cache. The first level
801 * is the language, the second level is the item or subitem.
803 * Since the data for a whole language is rebuilt in one operation, it needs
804 * to have a fast and atomic method for deleting or replacing all of the
805 * current data for a given language. The interface reflects this bulk update
806 * operation. Callers writing to the cache must first call startWrite(), then
807 * will call set() a couple of thousand times, then will call finishWrite()
808 * to commit the operation. When finishWrite() is called, the cache is
809 * expected to delete all data previously stored for that language.
811 * The values stored are PHP variables suitable for serialize(). Implementations
812 * of LCStore are responsible for serializing and unserializing.
814 interface LCStore {
816 * Get a value.
817 * @param $code string Language code
818 * @param $key string Cache key
820 function get( $code, $key );
823 * Start a write transaction.
824 * @param $code Language code
826 function startWrite( $code );
829 * Finish a write transaction.
831 function finishWrite();
834 * Set a key to a given value. startWrite() must be called before this
835 * is called, and finishWrite() must be called afterwards.
836 * @param $key
837 * @param $value
839 function set( $key, $value );
843 * LCStore implementation which uses PHP accelerator to store data.
844 * This will work if one of XCache, WinCache or APC cacher is configured.
845 * (See ObjectCache.php)
847 class LCStore_Accel implements LCStore {
848 var $currentLang;
849 var $keys;
851 public function __construct() {
852 $this->cache = wfGetCache( CACHE_ACCEL );
855 public function get( $code, $key ) {
856 $k = wfMemcKey( 'l10n', $code, 'k', $key );
857 $r = $this->cache->get( $k );
858 return $r === false ? null : $r;
861 public function startWrite( $code ) {
862 $k = wfMemcKey( 'l10n', $code, 'l' );
863 $keys = $this->cache->get( $k );
864 if ( $keys ) {
865 foreach ( $keys as $k ) {
866 $this->cache->delete( $k );
869 $this->currentLang = $code;
870 $this->keys = array();
873 public function finishWrite() {
874 if ( $this->currentLang ) {
875 $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
876 $this->cache->set( $k, array_keys( $this->keys ) );
878 $this->currentLang = null;
879 $this->keys = array();
882 public function set( $key, $value ) {
883 if ( $this->currentLang ) {
884 $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
885 $this->keys[$k] = true;
886 $this->cache->set( $k, $value );
892 * LCStore implementation which uses the standard DB functions to store data.
893 * This will work on any MediaWiki installation.
895 class LCStore_DB implements LCStore {
896 var $currentLang;
897 var $writesDone = false;
900 * @var DatabaseBase
902 var $dbw;
903 var $batch;
904 var $readOnly = false;
906 public function get( $code, $key ) {
907 if ( $this->writesDone ) {
908 $db = wfGetDB( DB_MASTER );
909 } else {
910 $db = wfGetDB( DB_SLAVE );
912 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
913 array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
914 if ( $row ) {
915 return unserialize( $row->lc_value );
916 } else {
917 return null;
921 public function startWrite( $code ) {
922 if ( $this->readOnly ) {
923 return;
926 if ( !$code ) {
927 throw new MWException( __METHOD__.": Invalid language \"$code\"" );
930 $this->dbw = wfGetDB( DB_MASTER );
931 try {
932 $this->dbw->begin( __METHOD__ );
933 $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
934 } catch ( DBQueryError $e ) {
935 if ( $this->dbw->wasReadOnlyError() ) {
936 $this->readOnly = true;
937 $this->dbw->rollback( __METHOD__ );
938 $this->dbw->ignoreErrors( false );
939 return;
940 } else {
941 throw $e;
945 $this->currentLang = $code;
946 $this->batch = array();
949 public function finishWrite() {
950 if ( $this->readOnly ) {
951 return;
954 if ( $this->batch ) {
955 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
958 $this->dbw->commit( __METHOD__ );
959 $this->currentLang = null;
960 $this->dbw = null;
961 $this->batch = array();
962 $this->writesDone = true;
965 public function set( $key, $value ) {
966 if ( $this->readOnly ) {
967 return;
970 if ( is_null( $this->currentLang ) ) {
971 throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
974 $this->batch[] = array(
975 'lc_lang' => $this->currentLang,
976 'lc_key' => $key,
977 'lc_value' => serialize( $value ) );
979 if ( count( $this->batch ) >= 100 ) {
980 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
981 $this->batch = array();
987 * LCStore implementation which stores data as a collection of CDB files in the
988 * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
989 * will throw an exception.
991 * Profiling indicates that on Linux, this implementation outperforms MySQL if
992 * the directory is on a local filesystem and there is ample kernel cache
993 * space. The performance advantage is greater when the DBA extension is
994 * available than it is with the PHP port.
996 * See Cdb.php and http://cr.yp.to/cdb.html
998 class LCStore_CDB implements LCStore {
999 var $readers, $writer, $currentLang, $directory;
1001 function __construct( $conf = array() ) {
1002 global $wgCacheDirectory;
1004 if ( isset( $conf['directory'] ) ) {
1005 $this->directory = $conf['directory'];
1006 } else {
1007 $this->directory = $wgCacheDirectory;
1011 public function get( $code, $key ) {
1012 if ( !isset( $this->readers[$code] ) ) {
1013 $fileName = $this->getFileName( $code );
1015 if ( !file_exists( $fileName ) ) {
1016 $this->readers[$code] = false;
1017 } else {
1018 $this->readers[$code] = CdbReader::open( $fileName );
1022 if ( !$this->readers[$code] ) {
1023 return null;
1024 } else {
1025 $value = $this->readers[$code]->get( $key );
1027 if ( $value === false ) {
1028 return null;
1030 return unserialize( $value );
1034 public function startWrite( $code ) {
1035 if ( !file_exists( $this->directory ) ) {
1036 if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
1037 throw new MWException( "Unable to create the localisation store " .
1038 "directory \"{$this->directory}\"" );
1042 // Close reader to stop permission errors on write
1043 if( !empty($this->readers[$code]) ) {
1044 $this->readers[$code]->close();
1047 $this->writer = CdbWriter::open( $this->getFileName( $code ) );
1048 $this->currentLang = $code;
1051 public function finishWrite() {
1052 // Close the writer
1053 $this->writer->close();
1054 $this->writer = null;
1055 unset( $this->readers[$this->currentLang] );
1056 $this->currentLang = null;
1059 public function set( $key, $value ) {
1060 if ( is_null( $this->writer ) ) {
1061 throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
1063 $this->writer->set( $key, serialize( $value ) );
1066 protected function getFileName( $code ) {
1067 if ( !$code || strpos( $code, '/' ) !== false ) {
1068 throw new MWException( __METHOD__.": Invalid language \"$code\"" );
1070 return "{$this->directory}/l10n_cache-$code.cdb";
1075 * Null store backend, used to avoid DB errors during install
1077 class LCStore_Null implements LCStore {
1078 public function get( $code, $key ) {
1079 return null;
1082 public function startWrite( $code ) {}
1083 public function finishWrite() {}
1084 public function set( $key, $value ) {}
1088 * A localisation cache optimised for loading large amounts of data for many
1089 * languages. Used by rebuildLocalisationCache.php.
1091 class LocalisationCache_BulkLoad extends LocalisationCache {
1093 * A cache of the contents of data files.
1094 * Core files are serialized to avoid using ~1GB of RAM during a recache.
1096 var $fileCache = array();
1099 * Most recently used languages. Uses the linked-list aspect of PHP hashtables
1100 * to keep the most recently used language codes at the end of the array, and
1101 * the language codes that are ready to be deleted at the beginning.
1103 var $mruLangs = array();
1106 * Maximum number of languages that may be loaded into $this->data
1108 var $maxLoadedLangs = 10;
1111 * @param $fileName
1112 * @param $fileType
1113 * @return array|mixed
1115 protected function readPHPFile( $fileName, $fileType ) {
1116 $serialize = $fileType === 'core';
1117 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
1118 $data = parent::readPHPFile( $fileName, $fileType );
1120 if ( $serialize ) {
1121 $encData = serialize( $data );
1122 } else {
1123 $encData = $data;
1126 $this->fileCache[$fileName][$fileType] = $encData;
1128 return $data;
1129 } elseif ( $serialize ) {
1130 return unserialize( $this->fileCache[$fileName][$fileType] );
1131 } else {
1132 return $this->fileCache[$fileName][$fileType];
1137 * @param $code
1138 * @param $key
1139 * @return mixed
1141 public function getItem( $code, $key ) {
1142 unset( $this->mruLangs[$code] );
1143 $this->mruLangs[$code] = true;
1144 return parent::getItem( $code, $key );
1148 * @param $code
1149 * @param $key
1150 * @param $subkey
1151 * @return
1153 public function getSubitem( $code, $key, $subkey ) {
1154 unset( $this->mruLangs[$code] );
1155 $this->mruLangs[$code] = true;
1156 return parent::getSubitem( $code, $key, $subkey );
1160 * @param $code
1162 public function recache( $code ) {
1163 parent::recache( $code );
1164 unset( $this->mruLangs[$code] );
1165 $this->mruLangs[$code] = true;
1166 $this->trimCache();
1170 * @param $code
1172 public function unload( $code ) {
1173 unset( $this->mruLangs[$code] );
1174 parent::unload( $code );
1178 * Unload cached languages until there are less than $this->maxLoadedLangs
1180 protected function trimCache() {
1181 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
1182 reset( $this->mruLangs );
1183 $code = key( $this->mruLangs );
1184 wfDebug( __METHOD__.": unloading $code\n" );
1185 $this->unload( $code );