Passed
Pull Request — master (#195)
by Marius
02:13
created

getEntity()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 15
nc 4
nop 4
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
	 */
40
	private $maxDepth;
41
42
	/**
43
	 * @var int Maximum number of entities to retrieve.
44
	 */
45
	private $maxEntityVisits;
46
47
	/**
48
	 * Map (entity id => true) of already visited entities.
49
	 *
50
	 * @var bool[]
51
	 */
52
	private $alreadyVisited = [];
53
54
	/**
55
	 * @param EntityLookup $entityLookup
56
	 * @param EntityPrefetcher $entityPrefetcher
57
	 * @param int $maxDepth Maximum search depth: Maximum number of intermediate entities to search through.
58
	 * @param int $maxEntityVisits Maximum number of entities to retrieve during a lookup.
59
	 */
60
	public function __construct(
61
		EntityLookup $entityLookup,
62
		EntityPrefetcher $entityPrefetcher,
63
		$maxDepth,
64
		$maxEntityVisits
65
	) {
66
		$this->entityLookup = $entityLookup;
67
		$this->entityPrefetcher = $entityPrefetcher;
68
		$this->maxDepth = $maxDepth;
69
		$this->maxEntityVisits = $maxEntityVisits;
70
	}
71
72
	/**
73
	 * Get the closest entity (out of $toIds), from a given entity. The starting entity, and
74
	 * the target entities are (potentially indirectly, via intermediate entities) linked by
75
	 * statements with the given property ID, pointing from the starting entity to one of the
76
	 * target entities.
77
	 *
78
	 * @since 3.10
79
	 *
80
	 * @param EntityId $fromId
81
	 * @param PropertyId $propertyId
82
	 * @param EntityId[] $toIds
83
	 *
84
	 * @return EntityId|null Returns null in case none of the target entities are referenced.
85
	 * @throws ReferencedEntityIdLookupException
86
	 */
87
	public function getReferencedEntityId( EntityId $fromId, PropertyId $propertyId, array $toIds ) {
88
		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...
89
			return null;
90
		}
91
92
		$this->alreadyVisited = [];
93
94
		$steps = $this->maxDepth + 1; // Add one as checking $fromId already is a step
95
		$toVisit = [ $fromId ];
96
97
		while ( $steps-- ) {
98
			$this->entityPrefetcher->prefetch( $toVisit );
99
			$toVisitNext = [];
100
101
			foreach ( $toVisit as $curId ) {
102
				$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...
103
				if ( $result ) {
104
					return $result;
105
				}
106
			}
107
			// Remove already visitted entities
108
			$toVisit = array_unique(
109
				array_diff( $toVisitNext, array_keys( $this->alreadyVisited ) )
110
			);
111
112
			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...
113
				return null;
114
			}
115
		}
116
117
		// Exhausted the max. depth without finding anything.
118
		throw new MaxReferenceDepthExhaustedException(
119
			$fromId,
120
			$propertyId,
121
			$toIds,
122
			$this->maxDepth
123
		);
124
	}
125
126
	/**
127
	 * Find out whether an entity (directly) references one of the target ids.
128
	 *
129
	 * @param EntityId $id Id of the entity to process
130
	 * @param EntityId $fromId Id this lookup started from
131
	 * @param PropertyId $propertyId
132
	 * @param EntityId[] $toIds
133
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
134
	 * @return EntityId|null Target id the entity refers to, null if none.
135
	 */
136
	private function processEntityById(
137
		EntityId $id,
138
		EntityId $fromId,
139
		PropertyId $propertyId,
140
		array $toIds,
141
		array &$toVisit
142
	) {
143
		$entity = $this->getEntity( $id, $fromId, $propertyId, $toIds );
144
		if ( !$entity ) {
145
			return null;
146
		}
147
148
		$mainSnaks = $this->getMainSnaks( $entity, $propertyId );
149
150
		foreach ( $mainSnaks as $mainSnak ) {
151
			$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...
152
			if ( $result ) {
153
				return $result;
154
			}
155
		}
156
157
		return null;
158
	}
159
160
	/**
161
	 * @param EntityId $id Id of the entity to get
162
	 * @param EntityId $fromId Id this lookup started from
163
	 * @param PropertyId $propertyId
164
	 * @param EntityId[] $toIds
165
	 *
166
	 * @return StatementListProvider|null Null if not applicable.
167
	 */
168
	private function getEntity( EntityId $id, EntityId $fromId, PropertyId $propertyId, array $toIds ) {
169
		$this->alreadyVisited[$id->getSerialization()] = true;
170
171
		if ( count( $this->alreadyVisited ) > $this->maxEntityVisits ) {
172
			throw new MaxReferencedEntityVisitsExhaustedException(
173
				$fromId,
174
				$propertyId,
175
				$toIds,
176
				$this->maxEntityVisits
177
			);
178
		}
179
180
		try {
181
			$entity = $this->entityLookup->getEntity( $id );
182
		} catch ( EntityLookupException $ex ) {
183
			throw new ReferencedEntityIdLookupException( $fromId, $propertyId, $toIds, null, $ex );
184
		}
185
186
		if ( !( $entity instanceof StatementListProvider ) ) {
187
			return null;
188
		}
189
190
		return $entity;
191
	}
192
193
	/**
194
	 * Decide whether a single Snak is pointing to one of the target ids.
195
	 *
196
	 * @param Snak $snak
197
	 * @param EntityId[] &$toVisit List of entities that still need to be checked
198
	 * @param EntityId[] $toIds
199
	 * @return EntityId|null Target id the Snak refers to, null if none.
200
	 */
201
	private function processSnak( Snak $snak, array &$toVisit, array $toIds ) {
202
		if ( ! ( $snak instanceof PropertyValueSnak ) ) {
203
			return null;
204
		}
205
		$dataValue = $snak->getDataValue();
206
		if ( ! ( $dataValue instanceof EntityIdValue ) ) {
207
			return null;
208
		}
209
210
		$entityId = $dataValue->getEntityId();
211
		if ( in_array( $entityId, $toIds, false ) ) {
212
			return $entityId;
213
		}
214
215
		$toVisit[] = $entityId;
216
217
		return null;
218
	}
219
220
	/**
221
	 * @param StatementListProvider $statementListProvider
222
	 * @param PropertyId $propertyId
223
	 * @return Snak[]
224
	 */
225
	private function getMainSnaks(
226
		StatementListProvider $statementListProvider,
227
		PropertyId $propertyId
228
	) {
229
		return $statementListProvider
230
			->getStatements()
231
			->getByPropertyId( $propertyId )
232
			->getBestStatements()
233
			->getMainSnaks();
234
	}
235
236
}
237