processSnak()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 4
nc 4
nop 3
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
class EntityRetrievingClosestReferencedEntityIdLookup implements ReferencedEntityIdLookup {
26
27
	/**
28
	 * @var EntityLookup
29
	 */
30
	private $entityLookup;
31
32
	/**
33
	 * @var EntityPrefetcher
34
	 */
35
	private $entityPrefetcher;
36
37
	/**
38
	 * @var int Maximum search depth: Maximum number of intermediate entities to search through.
39
	 *  For example 0 means that only the entities immediately referenced will be found.
40
	 */
41
	private $maxDepth;
42
43
	/**
44
	 * @var int Maximum number of entities to retrieve.
45
	 */
46
	private $maxEntityVisits;
47
48
	/**
49
	 * Map (entity id => true) of already visited entities.
50
	 *
51
	 * @var bool[]
52
	 */
53
	private $alreadyVisited = [];
54
55
	/**
56
	 * @param EntityLookup $entityLookup
57
	 * @param EntityPrefetcher $entityPrefetcher
58
	 * @param int $maxDepth Maximum search depth: Maximum number of intermediate entities to search through.
59
	 *  For example if 0 is given, only the entities immediately referenced will be found.
60
	 *  If this limit gets exhausted, a MaxReferenceDepthExhaustedException is thrown.
61
	 * @param int $maxEntityVisits Maximum number of entities to retrieve during a lookup.
62
	 *  If this limit gets exhausted, a MaxReferencedEntityVisitsExhaustedException is thrown.
63
	 */
64
	public function __construct(
65
		EntityLookup $entityLookup,
66
		EntityPrefetcher $entityPrefetcher,
67
		$maxDepth,
68
		$maxEntityVisits
69
	) {
70
		$this->entityLookup = $entityLookup;
71
		$this->entityPrefetcher = $entityPrefetcher;
72
		$this->maxDepth = $maxDepth;
73
		$this->maxEntityVisits = $maxEntityVisits;
74
	}
75
76
	/**
77
	 * Get the closest entity (out of $toIds), from a given entity. The starting entity, and
78
	 * the target entities are (potentially indirectly, via intermediate entities) linked by
79
	 * statements with the given property ID, pointing from the starting entity to one of the
80
	 * target entities.
81
	 *
82
	 * @since 3.10
83
	 *
84
	 * @param EntityId $fromId
85
	 * @param PropertyId $propertyId
86
	 * @param EntityId[] $toIds
87
	 *
88
	 * @return EntityId|null Returns null in case none of the target entities are referenced.
89
	 * @throws ReferencedEntityIdLookupException
90
	 */
91
	public function getReferencedEntityId( EntityId $fromId, PropertyId $propertyId, array $toIds ) {
92
		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...
93
			return null;
94
		}
95
96
		$this->alreadyVisited = [];
97
98
		$steps = $this->maxDepth + 1; // Add one as checking $fromId already is a step
99
		$toVisit = [ $fromId ];
100
101
		while ( $steps-- ) {
102
			$this->entityPrefetcher->prefetch( $toVisit );
103
			$toVisitNext = [];
104
105
			foreach ( $toVisit as $curId ) {
106
				$result = $this->processEntityById( $curId, $fromId, $propertyId, $toIds, $toVisitNext );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->processEntityById..., $toIds, $toVisitNext) (which targets Wikibase\DataModel\Servi...up::processEntityById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
107
				if ( $result ) {
108
					return $result;
109
				}
110
			}
111
			// Remove already visited entities
112
			$toVisit = array_unique(
113
				array_diff( $toVisitNext, array_keys( $this->alreadyVisited ) )
114
			);
115
116
			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...
117
				return null;
118
			}
119
		}
120
121
		// Exhausted the max. depth without finding anything.
122
		throw new MaxReferenceDepthExhaustedException(
123
			$fromId,
124
			$propertyId,
125
			$toIds,
126
			$this->maxDepth
127
		);
128
	}
129
130
	/**
131
	 * Find out whether an entity (directly) references one of the target ids.
132
	 *
133
	 * @param EntityId $id Id of the entity to process
134
	 * @param EntityId $fromId Id this lookup started from
135
	 * @param PropertyId $propertyId
136
	 * @param EntityId[] $toIds
137
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
138
	 * @return EntityId|null Target id the entity refers to, null if none.
139
	 */
140
	private function processEntityById(
141
		EntityId $id,
142
		EntityId $fromId,
143
		PropertyId $propertyId,
144
		array $toIds,
145
		array &$toVisit
146
	) {
147
		$entity = $this->getEntity( $id, $fromId, $propertyId, $toIds );
148
		if ( !$entity ) {
149
			return null;
150
		}
151
152
		$mainSnaks = $this->getMainSnaks( $entity, $propertyId );
153
154
		foreach ( $mainSnaks as $mainSnak ) {
155
			$result = $this->processSnak( $mainSnak, $toVisit, $toIds );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->processSnak($mainSnak, $toVisit, $toIds) (which targets Wikibase\DataModel\Servi...IdLookup::processSnak()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
156
			if ( $result ) {
157
				return $result;
158
			}
159
		}
160
161
		return null;
162
	}
163
164
	/**
165
	 * @param EntityId $id Id of the entity to get
166
	 * @param EntityId $fromId Id this lookup started from
167
	 * @param PropertyId $propertyId
168
	 * @param EntityId[] $toIds
169
	 *
170
	 * @return StatementListProvider|null Null if not applicable.
171
	 */
172
	private function getEntity( EntityId $id, EntityId $fromId, PropertyId $propertyId, array $toIds ) {
173
		if ( isset( $this->alreadyVisited[$id->getSerialization()] ) ) {
174
			trigger_error(
175
				'Entity ' . $id->getSerialization() . ' already visited.',
176
				E_USER_WARNING
177
			);
178
179
			return null;
180
		}
181
182
		$this->alreadyVisited[$id->getSerialization()] = true;
183
184
		if ( count( $this->alreadyVisited ) > $this->maxEntityVisits ) {
185
			throw new MaxReferencedEntityVisitsExhaustedException(
186
				$fromId,
187
				$propertyId,
188
				$toIds,
189
				$this->maxEntityVisits
190
			);
191
		}
192
193
		try {
194
			$entity = $this->entityLookup->getEntity( $id );
195
		} catch ( EntityLookupException $ex ) {
196
			throw new ReferencedEntityIdLookupException( $fromId, $propertyId, $toIds, null, $ex );
197
		}
198
199
		if ( !( $entity instanceof StatementListProvider ) ) {
200
			return null;
201
		}
202
203
		return $entity;
204
	}
205
206
	/**
207
	 * Decide whether a single Snak is pointing to one of the target ids.
208
	 *
209
	 * @param Snak $snak
210
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
211
	 * @param EntityId[] $toIds
212
	 * @return EntityId|null Target id the Snak refers to, null if none.
213
	 */
214
	private function processSnak( Snak $snak, array &$toVisit, array $toIds ) {
215
		if ( !( $snak instanceof PropertyValueSnak ) ) {
216
			return null;
217
		}
218
		$dataValue = $snak->getDataValue();
219
		if ( !( $dataValue instanceof EntityIdValue ) ) {
220
			return null;
221
		}
222
223
		$entityId = $dataValue->getEntityId();
224
		if ( in_array( $entityId, $toIds, false ) ) {
225
			return $entityId;
226
		}
227
228
		$toVisit[] = $entityId;
229
230
		return null;
231
	}
232
233
	/**
234
	 * @param StatementListProvider $statementListProvider
235
	 * @param PropertyId $propertyId
236
	 * @return Snak[]
237
	 */
238
	private function getMainSnaks(
239
		StatementListProvider $statementListProvider,
240
		PropertyId $propertyId
241
	) {
242
		return $statementListProvider
243
			->getStatements()
244
			->getByPropertyId( $propertyId )
245
			->getBestStatements()
246
			->getMainSnaks();
247
	}
248
249
}
250