LanguageFallbackChainFactory   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 3
dl 0
loc 324
rs 9.0399
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 4
A newFromLanguage() 0 13 2
A newFromLanguageCode() 0 13 2
C buildFromLanguage() 0 73 16
A newFromContext() 0 3 1
A newFromContextAndLanguageCode() 0 3 1
A newFromUserAndLanguageCode() 0 23 4
A getBabel() 0 25 3
A getBabelCategoryNames() 0 14 1
B buildFromBabel() 0 37 8

How to fix   Complexity   

Complex Class

Complex classes like LanguageFallbackChainFactory 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 LanguageFallbackChainFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Wikibase\Lib;
4
5
use Babel;
6
use ExtensionRegistry;
7
use IContextSource;
8
use InvalidArgumentException;
9
use Language;
10
use LanguageConverter;
11
use MediaWiki\Languages\LanguageConverterFactory;
12
use MediaWiki\Languages\LanguageFactory;
13
use MediaWiki\Languages\LanguageFallback;
14
use MediaWiki\MediaWikiServices;
15
use MWException;
16
use User;
17
18
/**
19
 * Object creating TermLanguageFallbackChain objects in Wikibase.
20
 *
21
 * @license GPL-2.0-or-later
22
 * @author Liangent < [email protected] >
23
 */
24
class LanguageFallbackChainFactory {
25
26
	/**
27
	 * Fallback levels
28
	 */
29
	const FALLBACK_ALL = 0xff;
30
31
	/**
32
	 * The language itself, e.g. 'en' for 'en'.
33
	 */
34
	const FALLBACK_SELF = 1;
35
36
	/**
37
	 * Other compatible languages that can be translated into the requested language
38
	 * (and translation is automatically done), e.g. 'sr', 'sr-ec' and 'sr-el' for 'sr'.
39
	 */
40
	const FALLBACK_VARIANTS = 2;
41
42
	/**
43
	 * All other language from the system fallback chain, e.g. 'de' and 'en' for 'de-formal'.
44
	 */
45
	const FALLBACK_OTHERS = 4;
46
47
	/** @var LanguageFactory */
48
	private $languageFactory;
49
50
	/** @var LanguageConverterFactory */
51
	private $languageConverterFactory;
52
53
	/** @var LanguageFallback */
54
	private $languageFallback;
55
56
	/**
57
	 * @var array[]
58
	 */
59
	private $languageCache = [];
60
61
	/**
62
	 * @var array[]
63
	 */
64
	private $userLanguageCache = [];
65
66
	public function __construct(
67
		?LanguageFactory $languageFactory = null,
68
		?LanguageConverterFactory $languageConverterFactory = null,
69
		?LanguageFallback $languageFallback = null
70
	) {
71
		$services = MediaWikiServices::getInstance();
72
		$this->languageFactory = $languageFactory ?: $services->getLanguageFactory();
73
		$this->languageConverterFactory = $languageConverterFactory ?: $services->getLanguageConverterFactory();
74
		$this->languageFallback = $languageFallback ?: $services->getLanguageFallback();
75
	}
76
77
	/**
78
	 * Get the fallback chain based a single language, and specified fallback level.
79
	 *
80
	 * @param Language $language
81
	 * @param int $mode Bitfield of self::FALLBACK_*
82
	 *
83
	 * @return TermLanguageFallbackChain
84
	 */
85
	public function newFromLanguage( Language $language, $mode = self::FALLBACK_ALL ) {
86
		$languageCode = $language->getCode();
87
88
		if ( !isset( $this->languageCache[$languageCode][$mode] ) ) {
89
			$chain = $this->buildFromLanguage( $language, $mode );
90
			$this->languageCache[$languageCode][$mode] = new TermLanguageFallbackChain(
91
				$chain,
0 ignored issues
show
Documentation introduced by
$chain is of type array<integer,object<Wik...anguageWithConversion>>, but the function expects a array<integer,object<Wik...anguageWithConversion>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
92
				WikibaseContentLanguages::getDefaultInstance()->getContentLanguages( WikibaseContentLanguages::CONTEXT_TERM )
93
			);
94
		}
95
96
		return $this->languageCache[$languageCode][$mode];
97
	}
98
99
	/**
100
	 * Get the fallback chain based a single language code, and specified fallback level.
101
	 *
102
	 * @param string $languageCode
103
	 * @param int $mode Bitfield of self::FALLBACK_*
104
	 *
105
	 * @return TermLanguageFallbackChain
106
	 */
107
	public function newFromLanguageCode( $languageCode, $mode = self::FALLBACK_ALL ) {
108
		$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
109
110
		if ( !isset( $this->languageCache[$languageCode][$mode] ) ) {
111
			$chain = $this->buildFromLanguage( $languageCode, $mode );
112
			$this->languageCache[$languageCode][$mode] = new TermLanguageFallbackChain(
113
				$chain,
0 ignored issues
show
Documentation introduced by
$chain is of type array<integer,object<Wik...anguageWithConversion>>, but the function expects a array<integer,object<Wik...anguageWithConversion>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
114
				WikibaseContentLanguages::getDefaultInstance()->getContentLanguages( WikibaseContentLanguages::CONTEXT_TERM )
115
			);
116
		}
117
118
		return $this->languageCache[$languageCode][$mode];
119
	}
120
121
	/**
122
	 * Build fallback chain array for a given language or validated language code.
123
	 *
124
	 * @param Language|string $language Language object or language code as string
125
	 * @param int $mode Bitfield of self::FALLBACK_*
126
	 * @param TermLanguageFallbackChain[] $chain for recursive calls
127
	 * @param bool[] $fetched for recursive calls
128
	 *
129
	 * @throws InvalidArgumentException
130
	 * @return LanguageWithConversion[]
131
	 */
132
	private function buildFromLanguage( $language, $mode, array $chain = [], array &$fetched = [] ) {
133
		if ( !is_int( $mode ) ) {
134
			throw new InvalidArgumentException( '$mode must be an integer' );
135
		}
136
137
		if ( is_string( $language ) ) {
138
			$languageCode = $language;
139
		} else {
140
			$languageCode = $language->getCode();
141
		}
142
143
		if ( $mode & self::FALLBACK_SELF ) {
144
			if ( !isset( $fetched[$languageCode] ) ) {
145
				$chain[] = LanguageWithConversion::factory( $language );
146
				$fetched[$languageCode] = true;
147
			}
148
		}
149
150
		if ( $mode & self::FALLBACK_VARIANTS ) {
151
			$parentLanguage = null;
152
			$pieces = explode( '-', $languageCode, 2 );
153
154
			if ( in_array( $pieces[0], LanguageConverter::$languagesWithVariants ) ) {
155
				if ( is_string( $language ) ) {
156
					$language = $this->languageFactory->getLanguage( $language );
157
				}
158
				$parentLanguage = $this->languageFactory->getParentLanguage( $language->getCode() );
159
			}
160
161
			if ( $parentLanguage ) {
162
				// It's less likely to trigger conversion mistakes by converting
163
				// zh-tw to zh-hk first instead of converting zh-cn to zh-tw.
164
				$parentLanguageConverter = $this->languageConverterFactory->getLanguageConverter( $parentLanguage );
165
				$variantFallbacks = $parentLanguageConverter->getVariantFallbacks( $languageCode );
166
				if ( is_array( $variantFallbacks ) ) {
167
					$variants = array_unique( array_merge(
168
						$variantFallbacks,
169
						$parentLanguageConverter->getVariants()
170
					) );
171
				} else {
172
					$variants = $parentLanguageConverter->getVariants();
173
				}
174
175
				foreach ( $variants as $variant ) {
176
					if ( !isset( $fetched[$variant] )
177
						// The self::FALLBACK_SELF mode is already responsible for self-references.
178
						&& $variant !== $languageCode
179
						&& $parentLanguageConverter->hasVariant( $variant )
180
					) {
181
						$chain[] = LanguageWithConversion::factory( $language, $variant );
182
						$fetched[$variant] = true;
183
					}
184
				}
185
			}
186
		}
187
188
		if ( $mode & self::FALLBACK_OTHERS ) {
189
			// Regarding $mode in recursive calls:
190
			// * self is a must to have the fallback item itself included;
191
			// * respect the original caller about whether to include variants or not;
192
			// * others should be excluded as they'll be handled here in loops.
193
			$recursiveMode = $mode;
194
			$recursiveMode &= self::FALLBACK_VARIANTS;
195
			$recursiveMode |= self::FALLBACK_SELF;
196
197
			$fallbacks = $this->languageFallback->getAll( $languageCode );
198
			foreach ( $fallbacks as $other ) {
199
				$chain = $this->buildFromLanguage( $other, $recursiveMode, $chain, $fetched );
0 ignored issues
show
Documentation introduced by
$chain is of type array<integer,object<Wik...anguageWithConversion>>, but the function expects a array<integer,object<Wik...LanguageFallbackChain>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
200
			}
201
		}
202
203
		return $chain;
204
	}
205
206
	/**
207
	 * Construct the fallback chain based on a context. Currently it just uses user and language info in it.
208
	 *
209
	 * @param IContextSource $context
210
	 *
211
	 * @return TermLanguageFallbackChain
212
	 */
213
	public function newFromContext( IContextSource $context ) {
214
		return $this->newFromUserAndLanguageCode( $context->getUser(), $context->getLanguage()->getCode() );
215
	}
216
217
	/**
218
	 * Construct the fallback chain based on a context, but ignore the language info in it and use a specified one instead.
219
	 *
220
	 * @param IContextSource $context
221
	 * @param string $languageCode
222
	 *
223
	 * @return TermLanguageFallbackChain
224
	 */
225
	public function newFromContextAndLanguageCode( IContextSource $context, $languageCode ) {
226
		return $this->newFromUserAndLanguageCode( $context->getUser(), $languageCode );
227
	}
228
229
	/**
230
	 * Construct the fallback chain based on a user and a language, currently from data provided by Extension:Babel.
231
	 *
232
	 * @param User $user
233
	 * @param string $languageCode
234
	 *
235
	 * @return TermLanguageFallbackChain
236
	 */
237
	public function newFromUserAndLanguageCode( User $user, $languageCode ) {
238
		if ( !ExtensionRegistry::getInstance()->isLoaded( 'Babel' ) || $user->isAnon() ) {
239
			return $this->newFromLanguageCode( $languageCode, self::FALLBACK_ALL );
240
		}
241
242
		$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
243
244
		if ( isset( $this->userLanguageCache[$user->getName()][$languageCode] ) ) {
245
			return $this->userLanguageCache[$user->getName()][$languageCode];
246
		}
247
248
		$babel = $this->getBabel( $languageCode, $user );
249
250
		$chain = $this->buildFromBabel( $babel );
251
		$languageFallbackChain = new TermLanguageFallbackChain(
252
			$chain,
253
			WikibaseContentLanguages::getDefaultInstance()->getContentLanguages( WikibaseContentLanguages::CONTEXT_TERM )
254
		);
255
256
		$this->userLanguageCache[$user->getName()][$languageCode] = $languageFallbackChain;
257
258
		return $languageFallbackChain;
259
	}
260
261
	private function getBabel( $languageCode, $user ) {
262
		$babel = [];
263
264
		$babelCategoryNames = $this->getBabelCategoryNames();
265
266
		if ( count( $babelCategoryNames ) ) {
267
			// A little redundant but it's the only way to get required information with current Babel API.
268
			$previousLevelBabel = [];
269
270
			foreach ( $babelCategoryNames as $level => $_ ) {
271
				// Make the current language at the top of the chain.
272
				$levelBabel = array_unique( array_merge(
273
					[ $languageCode ],
274
					Babel::getCachedUserLanguages( $user, $level )
275
				) );
276
277
				$babel[$level] = array_diff( $levelBabel, $previousLevelBabel );
278
				$previousLevelBabel = $levelBabel;
279
			}
280
		} else {
281
			$babel['N'] = [ $languageCode ];
282
		}
283
284
		return $babel;
285
	}
286
287
	private function getBabelCategoryNames() {
288
		global $wgBabelCategoryNames;
289
290
		$babelCategoryNames = array_filter(
291
			$wgBabelCategoryNames,
292
			function( $category ) {
293
				return $category !== false;
294
			}
295
		);
296
297
		krsort( $babelCategoryNames );
298
299
		return $babelCategoryNames;
300
	}
301
302
	/**
303
	 * Build fallback chain array for a given babel array.
304
	 *
305
	 * @param array $babel
306
	 *
307
	 * @return LanguageWithConversion[]
308
	 */
309
	public function buildFromBabel( array $babel ) {
310
		$chain = [];
311
		$fetched = [];
312
313
		// First pass to get "compatible" languages (self and variants)
314
		foreach ( $babel as $languageCodes ) { // Already sorted when added
315
			foreach ( [ self::FALLBACK_SELF, self::FALLBACK_VARIANTS ] as $mode ) {
316
				foreach ( $languageCodes as $languageCode ) {
317
					try {
318
						$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
319
					} catch ( MWException $e ) {
0 ignored issues
show
Bug introduced by
The class MWException does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
320
						continue;
321
					}
322
					$chain = $this->buildFromLanguage( $languageCode, $mode, $chain, $fetched );
323
				}
324
			}
325
		}
326
327
		// Second pass to get other languages from system fallback chain
328
		foreach ( $babel as $languageCodes ) {
329
			foreach ( $languageCodes as $languageCode ) {
330
				try {
331
					$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
332
				} catch ( MWException $e ) {
0 ignored issues
show
Bug introduced by
The class MWException does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
333
					continue;
334
				}
335
				$chain = $this->buildFromLanguage(
336
					$languageCode,
337
					self::FALLBACK_OTHERS | self::FALLBACK_VARIANTS,
338
					$chain,
339
					$fetched
340
				);
341
			}
342
		}
343
344
		return $chain;
345
	}
346
347
}
348