Completed
Push — master ( 0460d6...16f54e )
by no
05:35
created

SnakList   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 95.56%

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 4
dl 0
loc 309
ccs 43
cts 45
cp 0.9556
rs 8.2857
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 4
A hasSnakHash() 0 3 1
A removeSnakHash() 0 6 2
A addSnak() 0 8 2
A hasSnak() 0 3 1
A removeSnak() 0 3 1
A getSnak() 0 8 2
A equals() 0 8 3
A getHash() 0 4 1
A orderByProperty() 0 11 3
A moveSnaksToBottom() 0 6 2
A getSnaksByProperty() 0 14 3
A getNewOffset() 0 7 2
A offsetUnset() 0 12 2
A append() 0 3 1
A offsetSet() 0 3 1
A setElement() 0 17 4
A serialize() 0 6 1
A unserialize() 0 11 2
A isEmpty() 0 3 1
1
<?php
2
3
namespace Wikibase\DataModel\Snak;
4
5
use ArrayObject;
6
use Comparable;
7
use Hashable;
8
use InvalidArgumentException;
9
use Traversable;
10
use Wikibase\DataModel\Internal\MapValueHasher;
11
12
/**
13
 * List of Snak objects.
14
 * Indexes the snaks by hash and ensures no more the one snak with the same hash are in the list.
15
 *
16
 * @since 0.1
17
 *
18
 * @license GPL-2.0+
19
 * @author Jeroen De Dauw < [email protected] >
20
 * @author Addshore
21
 */
22
class SnakList extends ArrayObject implements Comparable, Hashable {
23
24
	/**
25
	 * Maps snak hashes to their offsets.
26
	 *
27 7
	 * @var array [ snak hash (string) => snak offset (string|int) ]
28 7
	 */
29
	private $offsetHashes = [];
30
31
	/**
32
	 * @var int
33
	 */
34
	private $indexOffset = 0;
35
36
	/**
37
	 * @param Snak[]|Traversable $snaks
38
	 *
39
	 * @throws InvalidArgumentException
40 3
	 */
41 3
	public function __construct( $snaks = [] ) {
42
		if ( !is_array( $snaks ) && !( $snaks instanceof Traversable ) ) {
43
			throw new InvalidArgumentException( '$snaks must be an array or an instance of Traversable' );
44
		}
45
46
		foreach ( $snaks as $index => $snak ) {
47
			$this->setElement( $index, $snak );
48
		}
49
	}
50
51 5
	/**
52 5
	 * @since 0.1
53 5
	 *
54
	 * @param string $snakHash
55
	 *
56
	 * @return boolean
57
	 */
58
	public function hasSnakHash( $snakHash ) {
59
		return array_key_exists( $snakHash, $this->offsetHashes );
60
	}
61
62
	/**
63
	 * @since 0.1
64 10
	 *
65 10
	 * @param string $snakHash
66
	 */
67
	public function removeSnakHash( $snakHash ) {
68
		if ( $this->hasSnakHash( $snakHash ) ) {
69
			$offset = $this->offsetHashes[$snakHash];
70
			$this->offsetUnset( $offset );
71
		}
72
	}
73
74
	/**
75
	 * @since 0.1
76
	 *
77 11
	 * @param Snak $snak
78 11
	 *
79
	 * @return boolean Indicates if the snak was added or not.
80
	 */
81
	public function addSnak( Snak $snak ) {
82
		if ( $this->hasSnak( $snak ) ) {
83
			return false;
84
		}
85
86
		$this->append( $snak );
87
		return true;
88 13
	}
89 13
90 13
	/**
91
	 * @since 0.1
92
	 *
93
	 * @param Snak $snak
94
	 *
95
	 * @return boolean
96
	 */
97
	public function hasSnak( Snak $snak ) {
98
		return $this->hasSnakHash( $snak->getHash() );
99
	}
100
101
	/**
102
	 * @since 0.1
103
	 *
104
	 * @param Snak $snak
105
	 */
106
	public function removeSnak( Snak $snak ) {
107
		$this->removeSnakHash( $snak->getHash() );
108
	}
109
110
	/**
111
	 * @since 0.1
112 7
	 *
113 7
	 * @param string $snakHash
114 7
	 *
115
	 * @return Snak|bool
116
	 */
117
	public function getSnak( $snakHash ) {
118
		if ( !$this->hasSnakHash( $snakHash ) ) {
119
			return false;
120
		}
121
122
		$offset = $this->offsetHashes[$snakHash];
123
		return $this->offsetGet( $offset );
124 7
	}
125 7
126 7
	/**
127
	 * @see Comparable::equals
128 7
	 *
129 6
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
130 5
	 *
131 5
	 * @since 0.3
132 5
	 *
133 7
	 * @param mixed $target
134 7
	 *
135
	 * @return bool
136
	 */
137
	public function equals( $target ) {
138
		if ( $this === $target ) {
139 5
			return true;
140 5
		}
141 5
142 5
		return $target instanceof self
143 5
			&& $this->getHash() === $target->getHash();
144 5
	}
145
146
	/**
147
	 * @see Hashable::getHash
148
	 *
149
	 * The hash is purely value based. Order of the elements in the array is not held into account.
150
	 *
151
	 * @since 0.1
152 7
	 *
153 7
	 * @return string
154
	 */
155 7
	public function getHash() {
156
		$hasher = new MapValueHasher();
157 5
		return $hasher->hash( $this );
158 5
	}
159 5
160 5
	/**
161 5
	 * Orders the snaks in the list grouping them by property.
162 7
	 *
163
	 * @param string[] $order List of serliazed property ids to order by.
164 7
	 *
165
	 * @since 0.5
166
	 */
167
	public function orderByProperty( array $order = [] ) {
168
		$snaksByProperty = $this->getSnaksByProperty();
169
		$orderedProperties = array_unique( array_merge( $order, array_keys( $snaksByProperty ) ) );
170
171
		foreach ( $orderedProperties as $property ) {
172
			if ( array_key_exists( $property, $snaksByProperty ) ) {
173
				$snaks = $snaksByProperty[$property];
174
				$this->moveSnaksToBottom( $snaks );
175
			}
176
		}
177
	}
178
179
	/**
180
	 * @param Snak[] $snaks to remove and re add
181
	 */
182
	private function moveSnaksToBottom( array $snaks ) {
183
		foreach ( $snaks as $snak ) {
184
			$this->removeSnak( $snak );
185
			$this->addSnak( $snak );
186
		}
187
	}
188
189
	/**
190
	 * Gets the snaks in the current object in an array
191
	 * grouped by property id
192
	 *
193
	 * @return array[]
194
	 */
195
	private function getSnaksByProperty() {
196
		$snaksByProperty = [];
197
198
		foreach ( $this as $snak ) {
199
			/** @var Snak $snak */
200
			$propertyId = $snak->getPropertyId()->getSerialization();
201
			if ( !isset( $snaksByProperty[$propertyId] ) ) {
202
				$snaksByProperty[$propertyId] = [];
203
			}
204
			$snaksByProperty[$propertyId][] = $snak;
205
		}
206
207
		return $snaksByProperty;
208
	}
209
210
	/**
211
	 * Finds a new offset for when appending an element.
212
	 * The base class does this, so it would be better to integrate,
213
	 * but there does not appear to be any way to do this...
214
	 *
215
	 * @return int
216
	 */
217
	private function getNewOffset() {
218
		while ( $this->offsetExists( $this->indexOffset ) ) {
219
			$this->indexOffset++;
220
		}
221
222
		return $this->indexOffset;
223
	}
224
225
	/**
226
	 * @see ArrayObject::offsetUnset
227
	 *
228
	 * @since 0.1
229
	 *
230
	 * @param int|string $index
231
	 */
232
	public function offsetUnset( $index ) {
233
		if ( $this->offsetExists( $index ) ) {
234
			/**
235
			 * @var Hashable $element
236
			 */
237
			$element = $this->offsetGet( $index );
238
			$hash = $element->getHash();
239
			unset( $this->offsetHashes[$hash] );
240
241
			parent::offsetUnset( $index );
242
		}
243
	}
244
245
	/**
246
	 * @see ArrayObject::append
247
	 *
248
	 * @param Snak $value
249
	 */
250
	public function append( $value ) {
251
		$this->setElement( null, $value );
252
	}
253
254
	/**
255
	 * @see ArrayObject::offsetSet()
256
	 *
257
	 * @param int|string $index
258
	 * @param Snak $value
259
	 */
260
	public function offsetSet( $index, $value ) {
261
		$this->setElement( $index, $value );
262
	}
263
264
	/**
265
	 * Method that actually sets the element and holds
266
	 * all common code needed for set operations, including
267
	 * type checking and offset resolving.
268
	 *
269
	 * @param int|string $index
270
	 * @param Snak $value
271
	 *
272
	 * @throws InvalidArgumentException
273
	 */
274
	private function setElement( $index, $value ) {
275
		if ( !( $value instanceof Snak ) ) {
276
			throw new InvalidArgumentException( '$value must be a Snak' );
277
		}
278
279
		if ( $this->hasSnak( $value ) ) {
280
			return;
281
		}
282
283
		if ( $index === null ) {
284
			$index = $this->getNewOffset();
285
		}
286
287
		$hash = $value->getHash();
288
		$this->offsetHashes[$hash] = $index;
289
		parent::offsetSet( $index, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of setElement()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
290
	}
291
292
	/**
293
	 * @see Serializable::serialize
294
	 *
295
	 * @return string
296
	 */
297
	public function serialize() {
298
		return serialize( [
299
			'data' => $this->getArrayCopy(),
300
			'index' => $this->indexOffset,
301
		] );
302
	}
303
304
	/**
305
	 * @see Serializable::unserialize
306
	 *
307
	 * @param string $serialized
308
	 */
309
	public function unserialize( $serialized ) {
310
		$serializationData = unserialize( $serialized );
311
312
		foreach ( $serializationData['data'] as $offset => $value ) {
313
			// Just set the element, bypassing checks and offset resolving,
314
			// as these elements have already gone through this.
315
			parent::offsetSet( $offset, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of unserialize()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
316
		}
317
318
		$this->indexOffset = $serializationData['index'];
319
	}
320
321
	/**
322
	 * Returns if the ArrayObject has no elements.
323
	 *
324
	 * @return bool
325
	 */
326
	public function isEmpty() {
327
		return !$this->getIterator()->valid();
328
	}
329
330
}
331