processEntityById()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 4
nop 5
dl 0
loc 22
rs 9.9666
1
<?php
2
3
namespace Wikibase\DataModel\Services\Lookup;
4
5
use Wikibase\DataModel\Entity\EntityId;
6
use Wikibase\DataModel\Entity\EntityIdValue;
7
use Wikibase\DataModel\Entity\PropertyId;
8
use Wikibase\DataModel\Services\Entity\EntityPrefetcher;
9
use Wikibase\DataModel\Snak\PropertyValueSnak;
10
use Wikibase\DataModel\Snak\Snak;
11
use Wikibase\DataModel\Statement\StatementListProvider;
12
13
/**
14
 * Service for getting the closest entity (out of a specified set),
15
 * from a given starting entity. The starting entity, and the target entities
16
 * are (potentially indirectly, via intermediate entities) linked by statements
17
 * with a given property ID, pointing from the starting entity to one of the
18
 * target entities.
19
 *
20
 * @since 3.10
21
 *
22
 * @license GPL-2.0-or-later
23
 * @author Marius Hoch
24
 *
25
 * @SuppressWarnings(PHPMD.LongClassName)
26
 */
27
class EntityRetrievingClosestReferencedEntityIdLookup implements ReferencedEntityIdLookup {
28
29
	/**
30
	 * @var EntityLookup
31
	 */
32
	private $entityLookup;
33
34
	/**
35
	 * @var EntityPrefetcher
36
	 */
37
	private $entityPrefetcher;
38
39
	/**
40
	 * @var int Maximum search depth: Maximum number of intermediate entities to search through.
41
	 *  For example 0 means that only the entities immediately referenced will be found.
42
	 */
43
	private $maxDepth;
44
45
	/**
46
	 * @var int Maximum number of entities to retrieve.
47
	 */
48
	private $maxEntityVisits;
49
50
	/**
51
	 * Map (entity id => true) of already visited entities.
52
	 *
53
	 * @var bool[]
54
	 */
55
	private $alreadyVisited = [];
56
57
	/**
58
	 * @param EntityLookup $entityLookup
59
	 * @param EntityPrefetcher $entityPrefetcher
60
	 * @param int $maxDepth Maximum search depth: Maximum number of intermediate entities to search through.
61
	 *  For example if 0 is given, only the entities immediately referenced will be found.
62
	 *  If this limit gets exhausted, a MaxReferenceDepthExhaustedException is thrown.
63
	 * @param int $maxEntityVisits Maximum number of entities to retrieve during a lookup.
64
	 *  If this limit gets exhausted, a MaxReferencedEntityVisitsExhaustedException is thrown.
65
	 */
66
	public function __construct(
67
		EntityLookup $entityLookup,
68
		EntityPrefetcher $entityPrefetcher,
69
		$maxDepth,
70
		$maxEntityVisits
71
	) {
72
		$this->entityLookup = $entityLookup;
73
		$this->entityPrefetcher = $entityPrefetcher;
74
		$this->maxDepth = $maxDepth;
75
		$this->maxEntityVisits = $maxEntityVisits;
76
	}
77
78
	/**
79
	 * Get the closest entity (out of $toIds), from a given entity. The starting entity, and
80
	 * the target entities are (potentially indirectly, via intermediate entities) linked by
81
	 * statements with the given property ID, pointing from the starting entity to one of the
82
	 * target entities.
83
	 *
84
	 * @since 3.10
85
	 *
86
	 * @param EntityId $fromId
87
	 * @param PropertyId $propertyId
88
	 * @param EntityId[] $toIds
89
	 *
90
	 * @return EntityId|null Returns null in case none of the target entities are referenced.
91
	 * @throws ReferencedEntityIdLookupException
92
	 */
93
	public function getReferencedEntityId( EntityId $fromId, PropertyId $propertyId, array $toIds ) {
94
		if ( !$toIds ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $toIds of type Wikibase\DataModel\Entity\EntityId[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
95
			return null;
96
		}
97
98
		$this->alreadyVisited = [];
99
100
		$steps = $this->maxDepth + 1; // Add one as checking $fromId already is a step
101
		$toVisit = [ $fromId ];
102
103
		while ( $steps-- ) {
104
			$this->entityPrefetcher->prefetch( $toVisit );
105
			$toVisitNext = [];
106
107
			foreach ( $toVisit as $curId ) {
108
				$result = $this->processEntityById( $curId, $fromId, $propertyId, $toIds, $toVisitNext );
109
				if ( $result ) {
110
					return $result;
111
				}
112
			}
113
			// Remove already visited entities
114
			$toVisit = array_unique(
115
				array_diff( $toVisitNext, array_keys( $this->alreadyVisited ) )
116
			);
117
118
			if ( !$toVisit ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $toVisit of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
119
				return null;
120
			}
121
		}
122
123
		// Exhausted the max. depth without finding anything.
124
		throw new MaxReferenceDepthExhaustedException(
125
			$fromId,
126
			$propertyId,
127
			$toIds,
128
			$this->maxDepth
129
		);
130
	}
131
132
	/**
133
	 * Find out whether an entity (directly) references one of the target ids.
134
	 *
135
	 * @param EntityId $id Id of the entity to process
136
	 * @param EntityId $fromId Id this lookup started from
137
	 * @param PropertyId $propertyId
138
	 * @param EntityId[] $toIds
139
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
140
	 * @return EntityId|null Target id the entity refers to, null if none.
141
	 */
142
	private function processEntityById(
143
		EntityId $id,
144
		EntityId $fromId,
145
		PropertyId $propertyId,
146
		array $toIds,
147
		array &$toVisit
148
	) {
149
		$entity = $this->getEntity( $id, $fromId, $propertyId, $toIds );
150
		if ( !$entity ) {
0 ignored issues
show
introduced by
$entity is of type Wikibase\DataModel\Statement\StatementListProvider, thus it always evaluated to true.
Loading history...
151
			return null;
152
		}
153
154
		$mainSnaks = $this->getMainSnaks( $entity, $propertyId );
155
156
		foreach ( $mainSnaks as $mainSnak ) {
157
			$result = $this->processSnak( $mainSnak, $toVisit, $toIds );
158
			if ( $result ) {
159
				return $result;
160
			}
161
		}
162
163
		return null;
164
	}
165
166
	/**
167
	 * @param EntityId $id Id of the entity to get
168
	 * @param EntityId $fromId Id this lookup started from
169
	 * @param PropertyId $propertyId
170
	 * @param EntityId[] $toIds
171
	 *
172
	 * @return StatementListProvider|null Null if not applicable.
173
	 */
174
	private function getEntity( EntityId $id, EntityId $fromId, PropertyId $propertyId, array $toIds ) {
175
		if ( isset( $this->alreadyVisited[$id->getSerialization()] ) ) {
176
			trigger_error(
177
				'Entity ' . $id->getSerialization() . ' already visited.',
178
				E_USER_WARNING
179
			);
180
181
			return null;
182
		}
183
184
		$this->alreadyVisited[$id->getSerialization()] = true;
185
186
		if ( count( $this->alreadyVisited ) > $this->maxEntityVisits ) {
187
			throw new MaxReferencedEntityVisitsExhaustedException(
188
				$fromId,
189
				$propertyId,
190
				$toIds,
191
				$this->maxEntityVisits
192
			);
193
		}
194
195
		try {
196
			$entity = $this->entityLookup->getEntity( $id );
197
		} catch ( EntityLookupException $ex ) {
198
			throw new ReferencedEntityIdLookupException( $fromId, $propertyId, $toIds, null, $ex );
199
		}
200
201
		if ( !( $entity instanceof StatementListProvider ) ) {
202
			return null;
203
		}
204
205
		return $entity;
206
	}
207
208
	/**
209
	 * Decide whether a single Snak is pointing to one of the target ids.
210
	 *
211
	 * @param Snak $snak
212
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
213
	 * @param EntityId[] $toIds
214
	 * @return EntityId|null Target id the Snak refers to, null if none.
215
	 */
216
	private function processSnak( Snak $snak, array &$toVisit, array $toIds ) {
217
		if ( !( $snak instanceof PropertyValueSnak ) ) {
218
			return null;
219
		}
220
		$dataValue = $snak->getDataValue();
221
		if ( !( $dataValue instanceof EntityIdValue ) ) {
222
			return null;
223
		}
224
225
		$entityId = $dataValue->getEntityId();
226
		if ( in_array( $entityId, $toIds, false ) ) {
227
			return $entityId;
228
		}
229
230
		$toVisit[] = $entityId;
231
232
		return null;
233
	}
234
235
	/**
236
	 * @param StatementListProvider $statementListProvider
237
	 * @param PropertyId $propertyId
238
	 * @return Snak[]
239
	 */
240
	private function getMainSnaks(
241
		StatementListProvider $statementListProvider,
242
		PropertyId $propertyId
243
	) {
244
		return $statementListProvider
245
			->getStatements()
246
			->getByPropertyId( $propertyId )
247
			->getBestStatements()
248
			->getMainSnaks();
249
	}
250
251
}
252