Test Failed
Push — moveSnaksToBottom-optimize2 ( 91f09b )
by no
02:16
created

SnakList   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 3
dl 0
loc 289
rs 9
c 0
b 0
f 0

18 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 20 4
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
	 * @var array [ snak hash (string) => snak offset (string|int) ]
28
	 */
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
	 */
41
	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
	/**
52
	 * @since 0.1
53
	 *
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
	 *
65
	 * @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
	 * @param Snak $snak
78
	 *
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
	}
89
90
	/**
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
	 *
113
	 * @param string $snakHash
114
	 *
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
	}
125
126
	/**
127
	 * @see Comparable::equals
128
	 *
129
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
130
	 *
131
	 * @since 0.3
132
	 *
133
	 * @param mixed $target
134
	 *
135
	 * @return bool
136
	 */
137
	public function equals( $target ) {
138
		if ( $this === $target ) {
139
			return true;
140
		}
141
142
		return $target instanceof self
143
			&& $this->getHash() === $target->getHash();
144
	}
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
	 *
153
	 * @return string
154
	 */
155
	public function getHash() {
156
		$hasher = new MapValueHasher();
157
		return $hasher->hash( $this );
158
	}
159
160
	/**
161
	 * Groups snaks by property, and optionally orders them.
162
	 *
163
	 * @param string[] $order List of property ID strings to order by. Snaks with other properties
164
	 *  will also be grouped, but put at the end, in the order each property appeared first in the
165
	 *  original list.
166
	 *
167
	 * @since 0.5
168
	 */
169
	public function orderByProperty( array $order = [] ) {
170
		$byProperty = array_combine( $order, array_fill( 0, count( $order ), [] ) );
171
172
		/** @var Snak $snak */
173
		foreach ( $this as $snak ) {
174
			$byProperty[$snak->getPropertyId()->getSerialization()][] = $snak;
175
		}
176
177
		$ordered = [];
178
		foreach ( $byProperty as $snaks ) {
179
			$ordered = array_merge( $ordered, $snaks );
180
		}
181
182
		$this->exchangeArray( $ordered );
183
184
		$index = 0;
185
		foreach ( $ordered as $snak ) {
186
			$this->offsetHashes[$snak->getHash()] = $index++;
187
		}
188
	}
189
190
	/**
191
	 * Finds a new offset for when appending an element.
192
	 * The base class does this, so it would be better to integrate,
193
	 * but there does not appear to be any way to do this...
194
	 *
195
	 * @return int
196
	 */
197
	private function getNewOffset() {
198
		while ( $this->offsetExists( $this->indexOffset ) ) {
199
			$this->indexOffset++;
200
		}
201
202
		return $this->indexOffset;
203
	}
204
205
	/**
206
	 * @see ArrayObject::offsetUnset
207
	 *
208
	 * @since 0.1
209
	 *
210
	 * @param int|string $index
211
	 */
212
	public function offsetUnset( $index ) {
213
		if ( $this->offsetExists( $index ) ) {
214
			/**
215
			 * @var Hashable $element
216
			 */
217
			$element = $this->offsetGet( $index );
218
			$hash = $element->getHash();
219
			unset( $this->offsetHashes[$hash] );
220
221
			parent::offsetUnset( $index );
222
		}
223
	}
224
225
	/**
226
	 * @see ArrayObject::append
227
	 *
228
	 * @param Snak $value
229
	 */
230
	public function append( $value ) {
231
		$this->setElement( null, $value );
232
	}
233
234
	/**
235
	 * @see ArrayObject::offsetSet()
236
	 *
237
	 * @param int|string $index
238
	 * @param Snak $value
239
	 */
240
	public function offsetSet( $index, $value ) {
241
		$this->setElement( $index, $value );
242
	}
243
244
	/**
245
	 * Method that actually sets the element and holds
246
	 * all common code needed for set operations, including
247
	 * type checking and offset resolving.
248
	 *
249
	 * @param int|string $index
250
	 * @param Snak $value
251
	 *
252
	 * @throws InvalidArgumentException
253
	 */
254
	private function setElement( $index, $value ) {
255
		if ( !( $value instanceof Snak ) ) {
256
			throw new InvalidArgumentException( '$value must be a Snak' );
257
		}
258
259
		if ( $this->hasSnak( $value ) ) {
260
			return;
261
		}
262
263
		if ( $index === null ) {
264
			$index = $this->getNewOffset();
265
		}
266
267
		$hash = $value->getHash();
268
		$this->offsetHashes[$hash] = $index;
269
		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...
270
	}
271
272
	/**
273
	 * @see Serializable::serialize
274
	 *
275
	 * @return string
276
	 */
277
	public function serialize() {
278
		return serialize( [
279
			'data' => $this->getArrayCopy(),
280
			'index' => $this->indexOffset,
281
		] );
282
	}
283
284
	/**
285
	 * @see Serializable::unserialize
286
	 *
287
	 * @param string $serialized
288
	 */
289
	public function unserialize( $serialized ) {
290
		$serializationData = unserialize( $serialized );
291
292
		foreach ( $serializationData['data'] as $offset => $value ) {
293
			// Just set the element, bypassing checks and offset resolving,
294
			// as these elements have already gone through this.
295
			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...
296
		}
297
298
		$this->indexOffset = $serializationData['index'];
299
	}
300
301
	/**
302
	 * Returns if the ArrayObject has no elements.
303
	 *
304
	 * @return bool
305
	 */
306
	public function isEmpty() {
307
		return !$this->getIterator()->valid();
308
	}
309
310
}
311