CachingPrefetchingTermLookup::getDescriptions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace Wikibase\Lib\Store;
6
7
use Psr\SimpleCache\CacheInterface;
8
use Wikibase\DataAccess\PrefetchingTermLookup;
9
use Wikibase\DataModel\Entity\EntityId;
10
use Wikibase\DataModel\Term\TermTypes;
11
use Wikibase\Lib\ContentLanguages;
12
13
/**
14
 * Prefetches terms from the Cache or via the provided PrefetchingTermLookup if not cached.
15
 *
16
 * Terms requested via the TermLookup methods are also buffered and cached.
17
 *
18
 * CacheInterface determines the medium of caching, and thus the availability (process, server, WAN).
19
 *
20
 * @license GPL-2.0-or-later
21
 */
22
final class CachingPrefetchingTermLookup implements PrefetchingTermLookup {
23
24
	use TermCacheKeyBuilder;
25
26
	private const DEFAULT_TTL = 60;
27
	private const RESOLVED_KEYS = 'resolvedKeys';
28
	private const UNRESOLVED_IDS = 'unresolvedIds';
29
	private const KEY_PARTS_MAP = 'keyPartsMap';
30
	private const UNCACHED_IDS = 'uncachedIds';
31
	private const UNCACHED_TERM_TYPES = 'uncachedTermTypes';
32
	private const UNCACHED_LANGUAGE_CODES = 'uncachedLanguageCodes';
33
	private const ENTITY_ID = 'entityId';
34
	private const TERM_TYPE = 'termType';
35
	private const LANGUAGE_CODE = 'languageCode';
36
37
	/**
38
	 * @var int
39
	 */
40
	private $cacheEntryTTL;
41
42
	/**
43
	 * @var CacheInterface
44
	 */
45
	private $cache;
46
47
	/**
48
	 * @var array
49
	 */
50
	private $prefetchedTerms;
51
52
	/**
53
	 * @var PrefetchingTermLookup
54
	 */
55
	private $lookup;
56
57
	/**
58
	 * @var RedirectResolvingLatestRevisionLookup
59
	 */
60
	private $redirectResolvingRevisionLookup;
61
62
	/**
63
	 * @var ContentLanguages
64
	 */
65
	private $termLanguages;
66
67
	public function __construct(
68
		CacheInterface $cache,
69
		PrefetchingTermLookup $lookup,
70
		RedirectResolvingLatestRevisionLookup $redirectResolvingRevisionLookup,
71
		ContentLanguages $termLanguages,
72
		?int $ttl = null
73
	) {
74
		$this->cache = $cache;
75
		$this->lookup = $lookup;
76
		$this->redirectResolvingRevisionLookup = $redirectResolvingRevisionLookup;
77
		$this->termLanguages = $termLanguages;
78
		$this->cacheEntryTTL = $ttl ?? self::DEFAULT_TTL;
79
	}
80
81
	public function prefetchTerms( array $entityIds, array $termTypes, array $languageCodes ): void {
82
		[
83
			self::UNCACHED_IDS => $uncachedIds,
0 ignored issues
show
Bug introduced by
The variable $uncachedIds does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
84
			self::UNCACHED_TERM_TYPES => $uncachedTermTypes,
0 ignored issues
show
Bug introduced by
The variable $uncachedTermTypes does not exist. Did you mean $termTypes?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
85
			self::UNCACHED_LANGUAGE_CODES => $uncachedLanguageCodes,
0 ignored issues
show
Bug introduced by
The variable $uncachedLanguageCodes does not exist. Did you mean $languageCodes?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
86
		] = $this->prefetchCachedTerms( $entityIds, $termTypes, $this->filterValidTermLanguages( $languageCodes ) );
87
88
		$this->prefetchAndCache( $uncachedIds, $uncachedTermTypes, $uncachedLanguageCodes );
0 ignored issues
show
Bug introduced by
The variable $uncachedTermTypes does not exist. Did you mean $termTypes?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $uncachedLanguageCodes does not exist. Did you mean $languageCodes?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
89
	}
90
91
	public function getPrefetchedTerm( EntityId $entityId, $termType, $languageCode ) {
92
		if ( isset( $this->prefetchedTerms[$entityId->getSerialization()][$termType][$languageCode] ) ) {
93
			return $this->prefetchedTerms[$entityId->getSerialization()][$termType][$languageCode];
94
		}
95
		return $this->lookup->getPrefetchedTerm( $entityId, $termType, $languageCode );
96
	}
97
98
	public function getPrefetchedAliases( EntityId $entityId, $languageCode ) {
99
		if ( isset( $this->prefetchedTerms[$entityId->getSerialization()][TermTypes::TYPE_ALIAS][$languageCode] ) ) {
100
			return $this->prefetchedTerms[$entityId->getSerialization()][TermTypes::TYPE_ALIAS][$languageCode];
101
		}
102
		return $this->lookup->getPrefetchedAliases( $entityId, $languageCode );
103
	}
104
105
	public function getLabel( EntityId $entityId, $languageCode ): ?string {
106
		if ( !$this->termLanguages->hasLanguage( $languageCode ) ) {
107
			return null;
108
		}
109
110
		return $this->getTerm( $entityId, $languageCode, TermTypes::TYPE_LABEL );
111
	}
112
113
	public function getDescription( EntityId $entityId, $languageCode ): ?string {
114
		if ( !$this->termLanguages->hasLanguage( $languageCode ) ) {
115
			return null;
116
		}
117
118
		return $this->getTerm( $entityId, $languageCode, TermTypes::TYPE_DESCRIPTION );
119
	}
120
121
	private function getTerm( EntityId $entityId, string $languageCode, string $termType ) {
122
		$cachedTerm = $this->getBufferedOrCachedEntry( $entityId, $termType, $languageCode );
123
		if ( $cachedTerm !== null ) {
124
			return $cachedTerm ?: null;
125
		}
126
127
		if ( $termType === TermTypes::TYPE_LABEL ) {
128
			$freshTerm = $this->lookup->getLabel( $entityId, $languageCode );
129
		} else {
130
			$freshTerm = $this->lookup->getDescription( $entityId, $languageCode );
131
		}
132
133
		if ( $freshTerm !== null ) {
134
			$this->bufferAndCacheExistingTerm( $entityId, $termType, $languageCode, $freshTerm );
135
		} else {
136
			$this->bufferAndCacheMissingTerm( $entityId, $termType, $languageCode );
137
		}
138
139
		return $freshTerm;
140
	}
141
142
	public function getLabels( EntityId $entityId, array $languageCodes ) {
143
		$validTermLanguageCodes = $this->filterValidTermLanguages( $languageCodes );
144
		return $this->getMultipleTermsByLanguage( $entityId, TermTypes::TYPE_LABEL, $validTermLanguageCodes );
145
	}
146
147
	public function getDescriptions( EntityId $entityId, array $languageCodes ) {
148
		$validTermLanguageCodes = $this->filterValidTermLanguages( $languageCodes );
149
		return $this->getMultipleTermsByLanguage( $entityId, TermTypes::TYPE_DESCRIPTION, $validTermLanguageCodes );
150
	}
151
152
	private function prefetchCachedTerms( array $entityIds, array $termTypes, array $languageCodes ): array {
153
		[
154
			self::RESOLVED_KEYS => $cacheKeys,
0 ignored issues
show
Bug introduced by
The variable $cacheKeys does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
155
			self::UNRESOLVED_IDS => $unresolvedIds, // This is intentionally unused.
0 ignored issues
show
Bug introduced by
The variable $unresolvedIds does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
156
			self::KEY_PARTS_MAP => $keyPartsMap,
0 ignored issues
show
Bug introduced by
The variable $keyPartsMap does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
157
		] = $this->getCacheKeys( $entityIds, $termTypes, $languageCodes );
158
		$cacheResults = $this->cache->getMultiple( $cacheKeys );
159
		$uncachedIds = [];
160
		$uncachedTermTypes = [];
161
		$uncachedLanguageCodes = [];
162
		foreach ( $cacheResults as $cacheKey => $cacheResult ) {
163
			$entityId = $keyPartsMap[$cacheKey][self::ENTITY_ID];
164
			$termType = $keyPartsMap[$cacheKey][self::TERM_TYPE];
165
			$languageCode = $keyPartsMap[$cacheKey][self::LANGUAGE_CODE];
166
			if ( $cacheResult === null ) {
167
				$uncachedIds[$entityId->getSerialization()] = $entityId;
168
				$uncachedTermTypes[$termType] = $termType;
169
				$uncachedLanguageCodes[$languageCode] = $languageCode;
170
				continue;
171
			}
172
173
			// we have data from the cache
174
			$this->setPrefetchedTermBuffer( $entityId, $termType, $languageCode, $cacheResult );
175
		}
176
177
		return [
178
			self::UNCACHED_IDS => array_values( $uncachedIds ),
179
			self::UNCACHED_TERM_TYPES => array_values( $uncachedTermTypes ),
180
			self::UNCACHED_LANGUAGE_CODES => array_values( $uncachedLanguageCodes ),
181
		];
182
	}
183
184
	private function getCacheKeys( array $entityIds, array $termTypes, array $languageCodes ): array {
185
		$cacheKeys = [];
186
		$unresolvedIds = [];
187
		$keyPartsMap = [];
188
		foreach ( $entityIds as $entityId ) {
189
			foreach ( $termTypes as $termType ) {
190
				foreach ( $languageCodes as $languageCode ) {
191
					$key = $this->getCacheKey( $entityId, $languageCode, $termType );
192
					if ( $key === null ) {
193
						$unresolvedIds[] = $key;
194
						continue 2; // problem resolving the redirect / looking up the latest revision
195
					}
196
					$cacheKeys[] = $key;
197
					$keyPartsMap[$key] = [
198
						self::ENTITY_ID => $entityId,
199
						self::TERM_TYPE => $termType,
200
						self::LANGUAGE_CODE => $languageCode,
201
					];
202
				}
203
			}
204
		}
205
		return [
206
			self::RESOLVED_KEYS => $cacheKeys,
207
			self::UNRESOLVED_IDS => $unresolvedIds,
208
			self::KEY_PARTS_MAP => $keyPartsMap,
209
		];
210
	}
211
212
	private function prefetchAndCache( array $uncachedIds, array $uncachedTermTypes, array $uncachedLanguageCodes ): void {
213
		$this->lookup->prefetchTerms( $uncachedIds, $uncachedTermTypes, $uncachedLanguageCodes );
214
		$this->cache->setMultiple( $this->getPrefetchedTermsFromLookup(
0 ignored issues
show
Documentation introduced by
$this->getPrefetchedTerm...es, $uncachedTermTypes) is of type array, but the function expects a object<Psr\SimpleCache\iterable>.

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...
215
			$uncachedIds,
216
			$uncachedLanguageCodes,
217
			$uncachedTermTypes
218
		), $this->cacheEntryTTL );
219
	}
220
221
	private function getPrefetchedTermsFromLookup(
222
		array $entitiesToPrefetch,
223
		array $languagesToPrefetch,
224
		array $termTypes
225
	): array {
226
		$terms = [];
227
228
		foreach ( $entitiesToPrefetch as $entity ) {
229
			foreach ( $languagesToPrefetch as $language ) {
230
				foreach ( $termTypes as $termType ) {
231
					$cacheKey = $this->getCacheKey( $entity, $language, $termType );
232
					if ( $cacheKey !== null ) {
233
						$terms[$cacheKey] = $this->getTermFromLookup( $entity, $termType, $language );
234
					}
235
				}
236
			}
237
		}
238
239
		return $terms;
240
	}
241
242
	private function getTermFromLookup( EntityId $entity, string $termType, string $language ) {
243
		if ( $termType === TermTypes::TYPE_LABEL || $termType === TermTypes::TYPE_DESCRIPTION ) {
244
			return $this->lookup->getPrefetchedTerm( $entity, $termType, $language );
245
		}
246
247
		return $this->lookup->getPrefetchedAliases( $entity, $language );
248
	}
249
250
	/**
251
	 * @param EntityId $entityId
252
	 * @param string $termType
253
	 * @param string $languageCode
254
	 * @param string|string[]|false $value string for existing label or description, string[] for existing aliases, false for term known
255
	 *                                     to not exist.
256
	 */
257
	private function setPrefetchedTermBuffer( EntityId $entityId, string $termType, string $languageCode, $value ): void {
258
		if ( !isset( $this->prefetchedTerms[$entityId->getSerialization()] ) ) {
259
			$this->prefetchedTerms[$entityId->getSerialization()] = [];
260
		}
261
		if ( !isset( $this->prefetchedTerms[$entityId->getSerialization()][$termType] ) ) {
262
			$this->prefetchedTerms[$entityId->getSerialization()][$termType] = [];
263
		}
264
265
		$this->prefetchedTerms[$entityId->getSerialization()][$termType][$languageCode] = $value;
266
	}
267
268
	/**
269
	 * @param EntityId $entityId
270
	 * @param string $termType
271
	 * @param string $languageCode
272
	 * @param string|string[] $freshTerm string for existing label or description, string[] for existing aliases
273
	 *                                   Should never be null or false @see bufferAndCacheMissingTerm
274
	 */
275
	private function bufferAndCacheExistingTerm( EntityId $entityId, string $termType, string $languageCode, $freshTerm ): void {
276
		$this->setPrefetchedTermBuffer( $entityId, $termType, $languageCode, $freshTerm );
277
		$this->cache->set(
278
			$this->getCacheKey( $entityId, $languageCode, $termType ),
279
			$freshTerm,
280
			$this->cacheEntryTTL
281
		);
282
	}
283
284
	private function bufferAndCacheMissingTerm( EntityId $entityId, string $termType, string $languageCode ): void {
285
		$this->setPrefetchedTermBuffer( $entityId, $termType, $languageCode, false );
286
		$cacheKey = $this->getCacheKey( $entityId, $languageCode, $termType );
287
		if ( $cacheKey === null ) {
288
			return;
289
		}
290
		$this->cache->set(
291
			$cacheKey,
292
			false,
293
			$this->cacheEntryTTL
294
		);
295
	}
296
297
	private function getCacheKey( EntityId $id, string $language, string $termType ) {
298
		$resolutionResult = $this->redirectResolvingRevisionLookup->lookupLatestRevisionResolvingRedirect( $id );
299
		if ( $resolutionResult === null ) {
300
			return null;
301
		}
302
303
		return $this->buildCacheKey( $resolutionResult[1], $resolutionResult[0], $language, $termType );
304
	}
305
306
	private function getBufferedOrCachedEntry( EntityId $entityId, string $termType, string $languageCode ) {
307
		// Check if it's prefetched already.
308
		$prefetchedTerm = $this->getPrefetchedTerm( $entityId, $termType, $languageCode );
309
		if ( $prefetchedTerm !== null ) {
310
			return $prefetchedTerm;
311
		}
312
313
		// Try getting it from cache
314
		$cacheKey = $this->getCacheKey( $entityId, $languageCode, $termType );
315
		return $cacheKey === null ? null : $this->cache->get( $cacheKey );
316
	}
317
318
	private function getMultipleTermsByLanguageFromBuffer( EntityId $entityId, string $termType, array $languages ) {
319
		$terms = [];
320
321
		// Lookup in termbuffer
322
		foreach ( $languages as $language ) {
323
			$prefetchedTerm = $this->getPrefetchedTerm( $entityId, $termType, $language );
324
			if ( $prefetchedTerm !== null ) {
325
				$terms[$language] = $prefetchedTerm;
326
			}
327
		}
328
329
		return $terms;
330
	}
331
332
	private function getMultipleTermsByLanguageFromCache( EntityId $entityId, string $termType, array $languages ) {
333
		$terms = [];
334
335
		$languagesToCacheKeys = [];
336
337
		foreach ( $languages as $language ) {
338
			$cacheKey = $this->getCacheKey( $entityId, $language, $termType );
339
			if ( $cacheKey ) {
340
				$languagesToCacheKeys[$language] = $cacheKey;
341
			}
342
		}
343
344
		$cacheKeysToLanguages = array_flip( $languagesToCacheKeys );
345
		$cacheTerms = $this->cache->getMultiple( array_values( $languagesToCacheKeys ) );
0 ignored issues
show
Documentation introduced by
array_values($languagesToCacheKeys) is of type array<integer,?>, but the function expects a object<Psr\SimpleCache\iterable>.

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...
346
347
		foreach ( $cacheTerms as $key => $term ) {
348
			if ( $term !== null ) {
349
				$terms[$cacheKeysToLanguages[$key]] = $term;
350
				$this->setPrefetchedTermBuffer( $entityId, $termType, $cacheKeysToLanguages[$key], $term );
351
			}
352
		}
353
354
		return $terms;
355
	}
356
357
	private function getMultipleTermsByLanguageFromLookup( EntityId $entityId, string $termType, array $languages ) {
358
		if ( $termType === TermTypes::TYPE_LABEL ) {
359
			$freshTerms = $this->lookup->getLabels( $entityId, $languages );
360
		} else {
361
			$freshTerms = $this->lookup->getDescriptions( $entityId, $languages );
362
		}
363
364
		foreach ( $languages as $language ) {
365
			if ( !isset( $freshTerms[$language] ) || !$freshTerms[$language] ) {
366
				$this->bufferAndCacheMissingTerm( $entityId, $termType, $language );
367
			} else {
368
				$this->bufferAndCacheExistingTerm( $entityId, $termType, $language, $freshTerms[$language] );
369
			}
370
		}
371
372
		return $freshTerms;
373
	}
374
375
	private function getMultipleTermsByLanguage( EntityId $entityId, string $termType, array $languages ) {
376
		$terms = $this->getMultipleTermsByLanguageFromBuffer( $entityId, $termType, $languages );
377
378
		// languages without prefetched terms
379
		$unbufferedLanguages = array_diff( $languages, array_keys( $terms ) );
380
		if ( empty( $unbufferedLanguages ) ) {
381
			return array_filter( $terms );
382
		}
383
384
		$terms = array_merge(
385
			$terms,
386
			$this->getMultipleTermsByLanguageFromCache( $entityId, $termType, $unbufferedLanguages )
387
		);
388
389
		$unCachedAndUnbufferedLanguages = array_values(
390
			array_diff( $languages, array_keys( $terms ) )
391
		);
392
		if ( empty( $unCachedAndUnbufferedLanguages ) ) {
393
			return array_filter( $terms );
394
		}
395
396
		$terms = array_merge(
397
			$terms,
398
			$this->getMultipleTermsByLanguageFromLookup( $entityId, $termType, $unCachedAndUnbufferedLanguages )
399
		);
400
401
		return array_filter( $terms );
402
	}
403
404
	private function filterValidTermLanguages( array $languageCodes ): array {
405
		return array_filter(
406
			$languageCodes,
407
			function ( $languageCode ) {
408
				return $this->termLanguages->hasLanguage( $languageCode );
409
			}
410
		);
411
	}
412
413
}
414