Complex classes like MessageCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MessageCache, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
35 | class MessageCache { |
||
36 | const FOR_UPDATE = 1; // force message reload |
||
37 | |||
38 | /** How long to wait for memcached locks */ |
||
39 | const WAIT_SEC = 15; |
||
40 | /** How long memcached locks last */ |
||
41 | const LOCK_TTL = 30; |
||
42 | |||
43 | /** |
||
44 | * Process local cache of loaded messages that are defined in |
||
45 | * MediaWiki namespace. First array level is a language code, |
||
46 | * second level is message key and the values are either message |
||
47 | * content prefixed with space, or !NONEXISTENT for negative |
||
48 | * caching. |
||
49 | * @var array $mCache |
||
50 | */ |
||
51 | protected $mCache; |
||
52 | |||
53 | /** |
||
54 | * Should mean that database cannot be used, but check |
||
55 | * @var bool $mDisable |
||
56 | */ |
||
57 | protected $mDisable; |
||
58 | |||
59 | /** |
||
60 | * Lifetime for cache, used by object caching. |
||
61 | * Set on construction, see __construct(). |
||
62 | */ |
||
63 | protected $mExpiry; |
||
64 | |||
65 | /** |
||
66 | * Message cache has its own parser which it uses to transform |
||
67 | * messages. |
||
68 | */ |
||
69 | protected $mParserOptions, $mParser; |
||
|
|||
70 | |||
71 | /** |
||
72 | * Variable for tracking which variables are already loaded |
||
73 | * @var array $mLoadedLanguages |
||
74 | */ |
||
75 | protected $mLoadedLanguages = []; |
||
76 | |||
77 | /** |
||
78 | * @var bool $mInParser |
||
79 | */ |
||
80 | protected $mInParser = false; |
||
81 | |||
82 | /** @var BagOStuff */ |
||
83 | protected $mMemc; |
||
84 | /** @var WANObjectCache */ |
||
85 | protected $wanCache; |
||
86 | |||
87 | /** |
||
88 | * Singleton instance |
||
89 | * |
||
90 | * @var MessageCache $instance |
||
91 | */ |
||
92 | private static $instance; |
||
93 | |||
94 | /** |
||
95 | * Get the signleton instance of this class |
||
96 | * |
||
97 | * @since 1.18 |
||
98 | * @return MessageCache |
||
99 | */ |
||
100 | public static function singleton() { |
||
112 | |||
113 | /** |
||
114 | * Destroy the singleton instance |
||
115 | * |
||
116 | * @since 1.18 |
||
117 | */ |
||
118 | public static function destroyInstance() { |
||
121 | |||
122 | /** |
||
123 | * Normalize message key input |
||
124 | * |
||
125 | * @param string $key Input message key to be normalized |
||
126 | * @return string Normalized message key |
||
127 | */ |
||
128 | public static function normalizeKey( $key ) { |
||
139 | |||
140 | /** |
||
141 | * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE. |
||
142 | * @param bool $useDB |
||
143 | * @param int $expiry Lifetime for cache. @see $mExpiry. |
||
144 | */ |
||
145 | function __construct( $memCached, $useDB, $expiry ) { |
||
164 | |||
165 | /** |
||
166 | * ParserOptions is lazy initialised. |
||
167 | * |
||
168 | * @return ParserOptions |
||
169 | */ |
||
170 | function getParserOptions() { |
||
189 | |||
190 | /** |
||
191 | * Try to load the cache from APC. |
||
192 | * |
||
193 | * @param string $code Optional language code, see documenation of load(). |
||
194 | * @return array|bool The cache array, or false if not in cache. |
||
195 | */ |
||
196 | protected function getLocalCache( $code ) { |
||
201 | |||
202 | /** |
||
203 | * Save the cache to APC. |
||
204 | * |
||
205 | * @param string $code |
||
206 | * @param array $cache The cache array |
||
207 | */ |
||
208 | protected function saveToLocalCache( $code, $cache ) { |
||
212 | |||
213 | /** |
||
214 | * Loads messages from caches or from database in this order: |
||
215 | * (1) local message cache (if $wgUseLocalMessageCache is enabled) |
||
216 | * (2) memcached |
||
217 | * (3) from the database. |
||
218 | * |
||
219 | * When succesfully loading from (2) or (3), all higher level caches are |
||
220 | * updated for the newest version. |
||
221 | * |
||
222 | * Nothing is loaded if member variable mDisable is true, either manually |
||
223 | * set by calling code or if message loading fails (is this possible?). |
||
224 | * |
||
225 | * Returns true if cache is already populated or it was succesfully populated, |
||
226 | * or false if populating empty cache fails. Also returns true if MessageCache |
||
227 | * is disabled. |
||
228 | * |
||
229 | * @param bool|string $code Language to which load messages |
||
230 | * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache |
||
231 | * @throws MWException |
||
232 | * @return bool |
||
233 | */ |
||
234 | function load( $code = false, $mode = null ) { |
||
370 | |||
371 | /** |
||
372 | * @param string $code |
||
373 | * @param array $where List of wfDebug() comments |
||
374 | * @param integer $mode Use MessageCache::FOR_UPDATE to use DB_MASTER |
||
375 | * @return bool|string True on success or one of ("cantacquire", "disabled") |
||
376 | */ |
||
377 | protected function loadFromDBWithLock( $code, array &$where, $mode = null ) { |
||
431 | |||
432 | /** |
||
433 | * Loads cacheable messages from the database. Messages bigger than |
||
434 | * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded |
||
435 | * on-demand from the database later. |
||
436 | * |
||
437 | * @param string $code Language code |
||
438 | * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache |
||
439 | * @return array Loaded messages for storing in caches |
||
440 | */ |
||
441 | function loadFromDB( $code, $mode = null ) { |
||
442 | global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; |
||
443 | |||
444 | $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE ); |
||
445 | |||
446 | $cache = []; |
||
447 | |||
448 | # Common conditions |
||
449 | $conds = [ |
||
450 | 'page_is_redirect' => 0, |
||
451 | 'page_namespace' => NS_MEDIAWIKI, |
||
452 | ]; |
||
453 | |||
454 | $mostused = []; |
||
455 | if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) { |
||
456 | if ( !isset( $this->mCache[$wgLanguageCode] ) ) { |
||
457 | $this->load( $wgLanguageCode ); |
||
458 | } |
||
459 | $mostused = array_keys( $this->mCache[$wgLanguageCode] ); |
||
460 | foreach ( $mostused as $key => $value ) { |
||
461 | $mostused[$key] = "$value/$code"; |
||
462 | } |
||
463 | } |
||
464 | |||
465 | if ( count( $mostused ) ) { |
||
466 | $conds['page_title'] = $mostused; |
||
467 | } elseif ( $code !== $wgLanguageCode ) { |
||
468 | $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code ); |
||
469 | } else { |
||
470 | # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses |
||
471 | # other than language code. |
||
472 | $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); |
||
473 | } |
||
474 | |||
475 | # Conditions to fetch oversized pages to ignore them |
||
476 | $bigConds = $conds; |
||
477 | $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ); |
||
478 | |||
479 | # Load titles for all oversized pages in the MediaWiki namespace |
||
480 | $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" ); |
||
481 | foreach ( $res as $row ) { |
||
482 | $cache[$row->page_title] = '!TOO BIG'; |
||
483 | } |
||
484 | |||
485 | # Conditions to load the remaining pages with their contents |
||
486 | $smallConds = $conds; |
||
487 | $smallConds[] = 'page_latest=rev_id'; |
||
488 | $smallConds[] = 'rev_text_id=old_id'; |
||
489 | $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ); |
||
490 | |||
491 | $res = $dbr->select( |
||
492 | [ 'page', 'revision', 'text' ], |
||
493 | [ 'page_title', 'old_text', 'old_flags' ], |
||
494 | $smallConds, |
||
495 | __METHOD__ . "($code)-small" |
||
496 | ); |
||
497 | |||
498 | foreach ( $res as $row ) { |
||
499 | $text = Revision::getRevisionText( $row ); |
||
500 | if ( $text === false ) { |
||
501 | // Failed to fetch data; possible ES errors? |
||
502 | // Store a marker to fetch on-demand as a workaround... |
||
503 | $entry = '!TOO BIG'; |
||
504 | wfDebugLog( |
||
505 | 'MessageCache', |
||
506 | __METHOD__ |
||
507 | . ": failed to load message page text for {$row->page_title} ($code)" |
||
508 | ); |
||
509 | } else { |
||
510 | $entry = ' ' . $text; |
||
511 | } |
||
512 | $cache[$row->page_title] = $entry; |
||
513 | } |
||
514 | |||
515 | $cache['VERSION'] = MSG_CACHE_VERSION; |
||
516 | ksort( $cache ); |
||
517 | $cache['HASH'] = md5( serialize( $cache ) ); |
||
518 | $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); |
||
519 | |||
520 | return $cache; |
||
521 | } |
||
522 | |||
523 | /** |
||
524 | * Updates cache as necessary when message page is changed |
||
525 | * |
||
526 | * @param string|bool $title Name of the page changed (false if deleted) |
||
527 | * @param mixed $text New contents of the page. |
||
528 | */ |
||
529 | public function replace( $title, $text ) { |
||
530 | global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; |
||
531 | |||
532 | if ( $this->mDisable ) { |
||
533 | return; |
||
534 | } |
||
535 | |||
536 | list( $msg, $code ) = $this->figureMessage( $title ); |
||
537 | if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) { |
||
538 | // Content language overrides do not use the /<code> suffix |
||
539 | return; |
||
540 | } |
||
541 | |||
542 | // Note that if the cache is volatile, load() may trigger a DB fetch. |
||
543 | // In that case we reenter/reuse the existing cache key lock to avoid |
||
544 | // a self-deadlock. This is safe as no reads happen *directly* in this |
||
545 | // method between getReentrantScopedLock() and load() below. There is |
||
546 | // no risk of data "changing under our feet" for replace(). |
||
547 | $cacheKey = wfMemcKey( 'messages', $code ); |
||
548 | $scopedLock = $this->getReentrantScopedLock( $cacheKey ); |
||
549 | $this->load( $code, self::FOR_UPDATE ); |
||
550 | |||
551 | $titleKey = wfMemcKey( 'messages', 'individual', $title ); |
||
552 | if ( $text === false ) { |
||
553 | // Article was deleted |
||
554 | $this->mCache[$code][$title] = '!NONEXISTENT'; |
||
555 | $this->wanCache->delete( $titleKey ); |
||
556 | } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { |
||
557 | // Check for size |
||
558 | $this->mCache[$code][$title] = '!TOO BIG'; |
||
559 | $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry ); |
||
560 | } else { |
||
561 | $this->mCache[$code][$title] = ' ' . $text; |
||
562 | $this->wanCache->delete( $titleKey ); |
||
563 | } |
||
564 | |||
565 | // Mark this cache as definitely "latest" (non-volatile) so |
||
566 | // load() calls do try to refresh the cache with slave data |
||
567 | $this->mCache[$code]['LATEST'] = time(); |
||
568 | |||
569 | // Update caches if the lock was acquired |
||
570 | if ( $scopedLock ) { |
||
571 | $this->saveToCaches( $this->mCache[$code], 'all', $code ); |
||
572 | } |
||
573 | |||
574 | ScopedCallback::consume( $scopedLock ); |
||
575 | // Relay the purge to APC and other DCs |
||
576 | $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) ); |
||
577 | |||
578 | // Also delete cached sidebar... just in case it is affected |
||
579 | $codes = [ $code ]; |
||
580 | if ( $code === 'en' ) { |
||
581 | // Delete all sidebars, like for example on action=purge on the |
||
582 | // sidebar messages |
||
583 | $codes = array_keys( Language::fetchLanguageNames() ); |
||
584 | } |
||
585 | |||
586 | foreach ( $codes as $code ) { |
||
587 | $sidebarKey = wfMemcKey( 'sidebar', $code ); |
||
588 | $this->wanCache->delete( $sidebarKey ); |
||
589 | } |
||
590 | |||
591 | // Update the message in the message blob store |
||
592 | $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader(); |
||
593 | $blobStore = $resourceloader->getMessageBlobStore(); |
||
594 | $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) ); |
||
595 | |||
596 | Hooks::run( 'MessageCacheReplace', [ $title, $text ] ); |
||
597 | } |
||
598 | |||
599 | /** |
||
600 | * Is the given cache array expired due to time passing or a version change? |
||
601 | * |
||
602 | * @param array $cache |
||
603 | * @return bool |
||
604 | */ |
||
605 | protected function isCacheExpired( $cache ) { |
||
618 | |||
619 | /** |
||
620 | * Shortcut to update caches. |
||
621 | * |
||
622 | * @param array $cache Cached messages with a version. |
||
623 | * @param string $dest Either "local-only" to save to local caches only |
||
624 | * or "all" to save to all caches. |
||
625 | * @param string|bool $code Language code (default: false) |
||
626 | * @return bool |
||
627 | */ |
||
628 | protected function saveToCaches( array $cache, $dest, $code = false ) { |
||
641 | |||
642 | /** |
||
643 | * Get the md5 used to validate the local APC cache |
||
644 | * |
||
645 | * @param string $code |
||
646 | * @return array (hash or false, bool expiry/volatility status) |
||
647 | */ |
||
648 | protected function getValidationHash( $code ) { |
||
649 | $curTTL = null; |
||
650 | $value = $this->wanCache->get( |
||
651 | wfMemcKey( 'messages', $code, 'hash', 'v1' ), |
||
652 | $curTTL, |
||
653 | [ wfMemcKey( 'messages', $code ) ] |
||
654 | ); |
||
655 | |||
656 | if ( !$value ) { |
||
657 | // No hash found at all; cache must regenerate to be safe |
||
658 | $hash = false; |
||
659 | $expired = true; |
||
660 | } else { |
||
661 | $hash = $value['hash']; |
||
662 | if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) { |
||
663 | // Cache was recently updated via replace() and should be up-to-date |
||
664 | $expired = false; |
||
665 | } else { |
||
666 | // See if the "check" key was bumped after the hash was generated |
||
667 | $expired = ( $curTTL < 0 ); |
||
668 | } |
||
669 | } |
||
670 | |||
671 | return [ $hash, $expired ]; |
||
672 | } |
||
673 | |||
674 | /** |
||
675 | * Set the md5 used to validate the local disk cache |
||
676 | * |
||
677 | * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not |
||
678 | * be treated as "volatile" by getValidationHash() for the next few seconds |
||
679 | * |
||
680 | * @param string $code |
||
681 | * @param array $cache Cached messages with a version |
||
682 | */ |
||
683 | protected function setValidationHash( $code, array $cache ) { |
||
684 | $this->wanCache->set( |
||
685 | wfMemcKey( 'messages', $code, 'hash', 'v1' ), |
||
686 | [ |
||
687 | 'hash' => $cache['HASH'], |
||
688 | 'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0 |
||
689 | ], |
||
690 | WANObjectCache::TTL_INDEFINITE |
||
691 | ); |
||
692 | } |
||
693 | |||
694 | /** |
||
695 | * @param string $key A language message cache key that stores blobs |
||
696 | * @param integer $timeout Wait timeout in seconds |
||
697 | * @return null|ScopedCallback |
||
698 | */ |
||
699 | protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) { |
||
702 | |||
703 | /** |
||
704 | * Get a message from either the content language or the user language. |
||
705 | * |
||
706 | * First, assemble a list of languages to attempt getting the message from. This |
||
707 | * chain begins with the requested language and its fallbacks and then continues with |
||
708 | * the content language and its fallbacks. For each language in the chain, the following |
||
709 | * process will occur (in this order): |
||
710 | * 1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that. |
||
711 | * Note: for the content language, there is no /lang subpage. |
||
712 | * 2. Fetch from the static CDB cache. |
||
713 | * 3. If available, check the database for fallback language overrides. |
||
714 | * |
||
715 | * This process provides a number of guarantees. When changing this code, make sure all |
||
716 | * of these guarantees are preserved. |
||
717 | * * If the requested language is *not* the content language, then the CDB cache for that |
||
718 | * specific language will take precedence over the root database page ([[MW:msg]]). |
||
719 | * * Fallbacks will be just that: fallbacks. A fallback language will never be reached if |
||
720 | * the message is available *anywhere* in the language for which it is a fallback. |
||
721 | * |
||
722 | * @param string $key The message key |
||
723 | * @param bool $useDB If true, look for the message in the DB, false |
||
724 | * to use only the compiled l10n cache. |
||
725 | * @param bool|string|object $langcode Code of the language to get the message for. |
||
726 | * - If string and a valid code, will create a standard language object |
||
727 | * - If string but not a valid code, will create a basic language object |
||
728 | * - If boolean and false, create object from the current users language |
||
729 | * - If boolean and true, create object from the wikis content language |
||
730 | * - If language object, use it as given |
||
731 | * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang". |
||
732 | * |
||
733 | * @throws MWException When given an invalid key |
||
734 | * @return string|bool False if the message doesn't exist, otherwise the |
||
735 | * message (which can be empty) |
||
736 | */ |
||
737 | function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { |
||
807 | |||
808 | /** |
||
809 | * Given a language, try and fetch messages from that language. |
||
810 | * |
||
811 | * Will also consider fallbacks of that language, the site language, and fallbacks for |
||
812 | * the site language. |
||
813 | * |
||
814 | * @see MessageCache::get |
||
815 | * @param Language|StubObject $lang Preferred language |
||
816 | * @param string $lckey Lowercase key for the message (as for localisation cache) |
||
817 | * @param bool $useDB Whether to include messages from the wiki database |
||
818 | * @return string|bool The message, or false if not found |
||
819 | */ |
||
820 | protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) { |
||
835 | |||
836 | /** |
||
837 | * Given a language, try and fetch messages from that language and its fallbacks. |
||
838 | * |
||
839 | * @see MessageCache::get |
||
840 | * @param Language|StubObject $lang Preferred language |
||
841 | * @param string $lckey Lowercase key for the message (as for localisation cache) |
||
842 | * @param bool $useDB Whether to include messages from the wiki database |
||
843 | * @param bool[] $alreadyTried Contains true for each language that has been tried already |
||
844 | * @return string|bool The message, or false if not found |
||
845 | */ |
||
846 | private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) { |
||
893 | |||
894 | /** |
||
895 | * Get the message page name for a given language |
||
896 | * |
||
897 | * @param string $langcode |
||
898 | * @param string $uckey Uppercase key for the message |
||
899 | * @return string The page name |
||
900 | */ |
||
901 | private function getMessagePageName( $langcode, $uckey ) { |
||
910 | |||
911 | /** |
||
912 | * Get a message from the MediaWiki namespace, with caching. The key must |
||
913 | * first be converted to two-part lang/msg form if necessary. |
||
914 | * |
||
915 | * Unlike self::get(), this function doesn't resolve fallback chains, and |
||
916 | * some callers require this behavior. LanguageConverter::parseCachedTable() |
||
917 | * and self::get() are some examples in core. |
||
918 | * |
||
919 | * @param string $title Message cache key with initial uppercase letter. |
||
920 | * @param string $code Code denoting the language to try. |
||
921 | * @return string|bool The message, or false if it does not exist or on error |
||
922 | */ |
||
923 | public function getMsgFromNamespace( $title, $code ) { |
||
1009 | |||
1010 | /** |
||
1011 | * @param string $message |
||
1012 | * @param bool $interface |
||
1013 | * @param string $language Language code |
||
1014 | * @param Title $title |
||
1015 | * @return string |
||
1016 | */ |
||
1017 | function transform( $message, $interface = false, $language = null, $title = null ) { |
||
1042 | |||
1043 | /** |
||
1044 | * @return Parser |
||
1045 | */ |
||
1046 | function getParser() { |
||
1063 | |||
1064 | /** |
||
1065 | * @param string $text |
||
1066 | * @param Title $title |
||
1067 | * @param bool $linestart Whether or not this is at the start of a line |
||
1068 | * @param bool $interface Whether this is an interface message |
||
1069 | * @param Language|string $language Language code |
||
1070 | * @return ParserOutput|string |
||
1071 | */ |
||
1072 | public function parse( $text, $title = null, $linestart = true, |
||
1107 | |||
1108 | function disable() { |
||
1111 | |||
1112 | function enable() { |
||
1115 | |||
1116 | /** |
||
1117 | * Clear all stored messages. Mainly used after a mass rebuild. |
||
1118 | */ |
||
1119 | function clear() { |
||
1128 | |||
1129 | /** |
||
1130 | * @param string $key |
||
1131 | * @return array |
||
1132 | */ |
||
1133 | public function figureMessage( $key ) { |
||
1150 | |||
1151 | /** |
||
1152 | * Get all message keys stored in the message cache for a given language. |
||
1153 | * If $code is the content language code, this will return all message keys |
||
1154 | * for which MediaWiki:msgkey exists. If $code is another language code, this |
||
1155 | * will ONLY return message keys for which MediaWiki:msgkey/$code exists. |
||
1156 | * @param string $code Language code |
||
1157 | * @return array Array of message keys (strings) |
||
1158 | */ |
||
1159 | public function getAllMessageKeys( $code ) { |
||
1176 | } |
||
1177 |
Only declaring a single property per statement allows you to later on add doc comments more easily.
It is also recommended by PSR2, so it is a common style that many people expect.