PageTerms::getEntityToPageMap()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace Wikibase\Client\Api;
6
7
use ApiQuery;
8
use ApiQueryBase;
9
use ApiResult;
10
use InvalidArgumentException;
11
use Title;
12
use Wikibase\Client\WikibaseClient;
13
use Wikibase\DataAccess\AliasTermBuffer;
14
use Wikibase\DataModel\Entity\EntityId;
15
use Wikibase\DataModel\Services\Term\TermBuffer;
16
use Wikibase\Lib\Store\EntityIdLookup;
17
use Wikibase\Lib\TermIndexEntry;
18
19
/**
20
 * Provides wikibase terms (labels, descriptions, aliases, etc.) for local pages.
21
 * For example, if a data item has the label "Washington" and the description "capital
22
 * city of the US", and has a sitelink to the local page called "Washington DC", calling
23
 * pageterms with titles=Washington_DC would include that label and description
24
 * in the response.
25
 *
26
 * @note This closely mirrors the Repo entityterms API, except for the factory method.
27
 *
28
 * @license GPL-2.0-or-later
29
 * @author Daniel Kinzler
30
 */
31
class PageTerms extends ApiQueryBase {
32
33
	/**
34
	 * @todo Use LabelDescriptionLookup for labels/descriptions, so we can apply language fallback.
35
	 * @var TermBuffer|AliasTermBuffer
36
	 */
37
	private $termBuffer;
38
39
	/**
40
	 * @var EntityIdLookup
41
	 */
42
	private $idLookup;
43
44
	public function __construct(
45
		TermBuffer $termBuffer,
46
		EntityIdLookup $idLookup,
47
		ApiQuery $query,
48
		string $moduleName
49
	) {
50
		parent::__construct( $query, $moduleName, 'wbpt' );
51
		$this->termBuffer = $termBuffer;
52
		$this->idLookup = $idLookup;
53
	}
54
55
	public static function factory( ApiQuery $apiQuery, string $moduleName ): self {
56
		$client = WikibaseClient::getDefaultInstance();
57
		$termBuffer = $client->getTermBuffer();
58
		$entityIdLookup = $client->getEntityIdLookup();
59
60
		return new self(
61
			$termBuffer,
62
			$entityIdLookup,
63
			$apiQuery,
64
			$moduleName
65
		);
66
	}
67
68
	public function execute(): void {
69
		$params = $this->extractRequestParams();
70
71
		# Only operate on existing pages
72
		$titles = $this->getPageSet()->getGoodTitles();
73
		if ( !count( $titles ) ) {
74
			# Nothing to do
75
			return;
76
		}
77
78
		// NOTE: continuation relies on $titles being sorted by page ID.
79
		ksort( $titles );
80
81
		$continue = $params['continue'];
82
		$termTypes = $params['terms'] ?? TermIndexEntry::$validTermTypes;
83
84
		$pagesToEntityIds = $this->getEntityIdsForTitles( $titles, $continue );
85
		$entityToPageMap = $this->getEntityToPageMap( $pagesToEntityIds );
86
87
		$terms = $this->getTermsOfEntities( $pagesToEntityIds, $termTypes, $this->getLanguage()->getCode() );
88
89
		$termGroups = $this->groupTermsByPageAndType( $entityToPageMap, $terms );
90
91
		$this->addTermsToResult( $pagesToEntityIds, $termGroups );
92
	}
93
94
	/**
95
	 * @param EntityId[] $entityIds
96
	 * @param string[] $termTypes
97
	 * @param string $languageCode
98
	 *
99
	 * @return TermIndexEntry[]
100
	 */
101
	private function getTermsOfEntities( array $entityIds, array $termTypes, string $languageCode ): array {
102
		$this->termBuffer->prefetchTerms( $entityIds, $termTypes, [ $languageCode ] );
0 ignored issues
show
Bug introduced by
The method prefetchTerms does only exist in Wikibase\DataModel\Services\Term\TermBuffer, but not in Wikibase\DataAccess\AliasTermBuffer.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
103
104
		$terms = [];
105
		foreach ( $entityIds as $entityId ) {
106
			foreach ( $termTypes as $termType ) {
107
				if ( $termType !== 'alias' ) {
108
					$termText = $this->termBuffer->getPrefetchedTerm( $entityId, $termType, $languageCode );
0 ignored issues
show
Bug introduced by
The method getPrefetchedTerm does only exist in Wikibase\DataModel\Services\Term\TermBuffer, but not in Wikibase\DataAccess\AliasTermBuffer.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
109
					if ( $termText !== false && $termText !== null ) {
110
						$terms[] = new TermIndexEntry( [
111
							TermIndexEntry::FIELD_ENTITY => $entityId,
112
							TermIndexEntry::FIELD_TYPE => $termType,
113
							TermIndexEntry::FIELD_LANGUAGE => $languageCode,
114
							TermIndexEntry::FIELD_TEXT => $termText,
115
						] );
116
					}
117
				} else {
118
					$termTexts = $this->termBuffer->getPrefetchedAliases( $entityId, $languageCode ) ?: [];
0 ignored issues
show
Bug introduced by
The method getPrefetchedAliases does only exist in Wikibase\DataAccess\AliasTermBuffer, but not in Wikibase\DataModel\Services\Term\TermBuffer.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
119
					foreach ( $termTexts as $termText ) {
120
						$terms[] = new TermIndexEntry( [
121
							TermIndexEntry::FIELD_ENTITY => $entityId,
122
							TermIndexEntry::FIELD_TYPE => $termType,
123
							TermIndexEntry::FIELD_LANGUAGE => $languageCode,
124
							TermIndexEntry::FIELD_TEXT => $termText,
125
						] );
126
					}
127
				}
128
129
			}
130
		}
131
132
		return $terms;
133
	}
134
135
	/**
136
	 * @param Title[] $titles
137
	 * @param int|null $continue
138
	 *
139
	 * @return array
140
	 */
141
	private function getEntityIdsForTitles( array $titles, $continue = 0 ): array {
142
		$entityIds = $this->idLookup->getEntityIds( $titles );
143
144
		// Re-sort, so the order of page IDs matches the order in which $titles
145
		// were given. This is essential for paging to work properly.
146
		// This also skips all page IDs up to $continue.
147
		$sortedEntityId = [];
148
		foreach ( $titles as $pid => $title ) {
149
			if ( $pid >= $continue && isset( $entityIds[$pid] ) ) {
150
				$sortedEntityId[$pid] = $entityIds[$pid];
151
			}
152
		}
153
154
		return $sortedEntityId;
155
	}
156
157
	/**
158
	 * @param EntityId[] $entityIds
159
	 *
160
	 * @return int[]
161
	 */
162
	private function getEntityToPageMap( array $entityIds ): array {
163
		$entityIdsStrings = array_map(
164
			function( EntityId $id ) {
165
				return $id->getSerialization();
166
			},
167
			$entityIds
168
		);
169
170
		return array_flip( $entityIdsStrings );
171
	}
172
173
	/**
174
	 * @param int[] $entityToPageMap
175
	 * @param TermIndexEntry[] $terms
176
	 *
177
	 * @return array[] An associative array, mapping pageId + entity type to a list of strings.
178
	 */
179
	private function groupTermsByPageAndType( array $entityToPageMap, array $terms ): array {
180
		$termsPerPage = [];
181
182
		foreach ( $terms as $term ) {
183
			// Since we construct $terms and $entityToPageMap from the same set of page IDs,
184
			// the entry $entityToPageMap[$key] should really always be set.
185
			$type = $term->getTermType();
186
			$key = $term->getEntityId()->getSerialization();
187
			$pageId = $entityToPageMap[$key];
188
			$text = $term->getText();
189
190
			if ( $text !== null ) {
191
				// For each page ID, record a list of terms for each term type.
192
				$termsPerPage[$pageId][$type][] = $text;
193
			} else {
194
				// $text should never be null, but let's be vigilant.
195
				wfWarn( __METHOD__ . ': Encountered null text in TermIndexEntry object!' );
196
			}
197
		}
198
199
		return $termsPerPage;
200
	}
201
202
	/**
203
	 * @param EntityId[] $pagesToEntityIds
204
	 * @param array[] $termGroups
205
	 */
206
	private function addTermsToResult( array $pagesToEntityIds, array $termGroups ): void {
207
		$result = $this->getResult();
208
209
		foreach ( $pagesToEntityIds as $currentPage => $entityId ) {
210
			if ( !isset( $termGroups[$currentPage] ) ) {
211
				// No entity for page, or no terms for entity.
212
				continue;
213
			}
214
215
			$group = $termGroups[$currentPage];
216
217
			if ( !$this->addTermsForPage( $result, $currentPage, $group ) ) {
218
				break;
219
			}
220
		}
221
	}
222
223
	/**
224
	 * Add page term to an ApiResult, adding a continue
225
	 * parameter if it doesn't fit.
226
	 *
227
	 * @param ApiResult $result
228
	 * @param int $pageId
229
	 * @param array[] $termsByType
230
	 *
231
	 * @throws InvalidArgumentException
232
	 * @return bool True if it fits in the result
233
	 */
234
	private function addTermsForPage( ApiResult $result, int $pageId, array $termsByType ): bool {
235
		ApiResult::setIndexedTagNameRecursive( $termsByType, 'term' );
236
237
		$fit = $result->addValue( [ 'query', 'pages', $pageId ], 'terms', $termsByType );
238
239
		if ( !$fit ) {
240
			$this->setContinueEnumParameter( 'continue', $pageId );
241
		}
242
243
		return $fit;
244
	}
245
246
	/**
247
	 * @see ApiQueryBase::getCacheMode
248
	 *
249
	 * @param array $params
250
	 * @return string
251
	 */
252
	public function getCacheMode( $params ): string {
253
		return 'public';
254
	}
255
256
	/**
257
	 * @inheritDoc
258
	 */
259
	protected function getAllowedParams(): array {
260
		return [
261
			'continue' => [
262
				self::PARAM_HELP_MSG => 'api-help-param-continue',
263
				self::PARAM_TYPE => 'integer',
264
			],
265
			'terms' => [
266
				self::PARAM_TYPE => TermIndexEntry::$validTermTypes,
267
				self::PARAM_DFLT => implode( '|',  TermIndexEntry::$validTermTypes ),
268
				self::PARAM_ISMULTI => true,
269
				self::PARAM_HELP_MSG => 'apihelp-query+pageterms-param-terms',
270
			],
271
		];
272
	}
273
274
	/**
275
	 * @inheritDoc
276
	 */
277
	protected function getExamplesMessages(): array {
278
		return [
279
			'action=query&prop=pageterms&titles=London'
280
				=> 'apihelp-query+pageterms-example-simple',
281
			'action=query&prop=pageterms&titles=London&wbptterms=label|alias&uselang=en'
282
				=> 'apihelp-query+pageterms-example-label-en',
283
		];
284
	}
285
286
}
287