Passed
Push — rm-hasharray ( 016649...666d9f )
by no
03:22
created

SnakList::setElement()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 4
nop 2
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 Hashable, Comparable {
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
	 * @see ArrayObject::__construct
38
	 *
39
	 * @param array|Traversable|null $input
40
	 * @param int $flags
41
	 * @param string $iteratorClass
42
	 *
43
	 * @throws InvalidArgumentException
44
	 */
45
	public function __construct( $input = null, $flags = 0, $iteratorClass = 'ArrayIterator' ) {
46
		parent::__construct( [], $flags, $iteratorClass );
47
48
		if ( $input !== null ) {
49
			if ( !is_array( $input ) && !( $input instanceof Traversable ) ) {
50
				throw new InvalidArgumentException( '$input must be an array or Traversable' );
51
			}
52
53
			foreach ( $input as $offset => $value ) {
54
				$this->offsetSet( $offset, $value );
55
			}
56
		}
57
	}
58
59
	/**
60
	 * Finds a new offset for when appending an element.
61
	 * The base class does this, so it would be better to integrate,
62
	 * but there does not appear to be any way to do this...
63
	 *
64
	 * @return int
65
	 */
66
	private function getNewOffset() {
67
		while ( $this->offsetExists( $this->indexOffset ) ) {
68
			$this->indexOffset++;
69
		}
70
71
		return $this->indexOffset;
72
	}
73
74
	/**
75
	 * @see ArrayObject::offsetUnset
76
	 *
77
	 * @since 0.1
78
	 *
79
	 * @param int|string $index
80
	 */
81
	public function offsetUnset( $index ) {
82
		if ( $this->offsetExists( $index ) ) {
83
			/**
84
			 * @var Hashable $element
85
			 */
86
			$element = $this->offsetGet( $index );
87
			$hash = $element->getHash();
88
			unset( $this->offsetHashes[$hash] );
89
90
			parent::offsetUnset( $index );
91
		}
92
	}
93
94
	/**
95
	 * @see Hashable::getHash
96
	 *
97
	 * The hash is purely valuer based. Order of the elements in the array is not held into account.
98
	 *
99
	 * @since 0.1
100
	 *
101
	 * @return string
102
	 */
103
	public function getHash() {
104
		$hasher = new MapValueHasher();
105
		return $hasher->hash( $this );
106
	}
107
108
	/**
109
	 * @see Comparable::equals
110
	 *
111
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
112
	 *
113
	 * @since 0.3
114
	 *
115
	 * @param mixed $target
116
	 *
117
	 * @return bool
118
	 */
119
	public function equals( $target ) {
120
		if ( $this === $target ) {
121
			return true;
122
		}
123
124
		return $target instanceof self
125
			&& $this->getHash() === $target->getHash();
126
	}
127
128
	/**
129
	 * @see ArrayObject::append
130
	 *
131
	 * @param Snak $value
132
	 */
133
	public function append( $value ) {
134
		$this->setElement( null, $value );
135
	}
136
137
	/**
138
	 * @see ArrayObject::offsetSet()
139
	 *
140
	 * @param int|string $index
141
	 * @param Snak $value
142
	 */
143
	public function offsetSet( $index, $value ) {
144
		$this->setElement( $index, $value );
145
	}
146
147
	/**
148
	 * Method that actually sets the element and holds
149
	 * all common code needed for set operations, including
150
	 * type checking and offset resolving.
151
	 *
152
	 * @param int|string $index
153
	 * @param Snak $value
154
	 *
155
	 * @throws InvalidArgumentException
156
	 */
157
	private function setElement( $index, $value ) {
158
		if ( !( $value instanceof Snak ) ) {
159
			throw new InvalidArgumentException( '$value must be a Snak' );
160
		}
161
162
		if ( $this->hasSnak( $value ) ) {
163
			return;
164
		}
165
166
		if ( $index === null ) {
167
			$index = $this->getNewOffset();
168
		}
169
170
		$hash = $value->getHash();
171
		$this->offsetHashes[$hash] = $index;
172
		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...
173
	}
174
175
	/**
176
	 * @see Serializable::serialize
177
	 *
178
	 * @return string
179
	 */
180
	public function serialize() {
181
		return serialize( [
182
			'data' => $this->getArrayCopy(),
183
			'index' => $this->indexOffset,
184
		] );
185
	}
186
187
	/**
188
	 * @see Serializable::unserialize
189
	 *
190
	 * @param string $serialized
191
	 */
192
	public function unserialize( $serialized ) {
193
		$serializationData = unserialize( $serialized );
194
195
		foreach ( $serializationData['data'] as $offset => $value ) {
196
			// Just set the element, bypassing checks and offset resolving,
197
			// as these elements have already gone through this.
198
			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...
199
		}
200
201
		$this->indexOffset = $serializationData['index'];
202
	}
203
204
	/**
205
	 * Returns if the ArrayObject has no elements.
206
	 *
207
	 * @return bool
208
	 */
209
	public function isEmpty() {
210
		return !$this->getIterator()->valid();
211
	}
212
213
	/**
214
	 * @since 0.1
215
	 *
216
	 * @param string $snakHash
217
	 *
218
	 * @return boolean
219
	 */
220
	public function hasSnakHash( $snakHash ) {
221
		return array_key_exists( $snakHash, $this->offsetHashes );
222
	}
223
224
	/**
225
	 * @since 0.1
226
	 *
227
	 * @param string $snakHash
228
	 */
229
	public function removeSnakHash( $snakHash ) {
230
		if ( $this->hasSnakHash( $snakHash ) ) {
231
			$offset = $this->offsetHashes[$snakHash];
232
			$this->offsetUnset( $offset );
233
		}
234
	}
235
236
	/**
237
	 * @since 0.1
238
	 *
239
	 * @param Snak $snak
240
	 *
241
	 * @return boolean Indicates if the snak was added or not.
242
	 */
243
	public function addSnak( Snak $snak ) {
244
		if ( $this->hasSnak( $snak ) ) {
245
			return false;
246
		}
247
248
		$this->append( $snak );
249
		return true;
250
	}
251
252
	/**
253
	 * @since 0.1
254
	 *
255
	 * @param Snak $snak
256
	 *
257
	 * @return boolean
258
	 */
259
	public function hasSnak( Snak $snak ) {
260
		return $this->hasSnakHash( $snak->getHash() );
261
	}
262
263
	/**
264
	 * @since 0.1
265
	 *
266
	 * @param Snak $snak
267
	 */
268
	public function removeSnak( Snak $snak ) {
269
		$this->removeSnakHash( $snak->getHash() );
270
	}
271
272
	/**
273
	 * @since 0.1
274
	 *
275
	 * @param string $snakHash
276
	 *
277
	 * @return Snak|bool
278
	 */
279
	public function getSnak( $snakHash ) {
280
		if ( !$this->hasSnakHash( $snakHash ) ) {
281
			return false;
282
		}
283
284
		$offset = $this->offsetHashes[$snakHash];
285
		return $this->offsetGet( $offset );
286
	}
287
288
	/**
289
	 * Orders the snaks in the list grouping them by property.
290
	 *
291
	 * @param string[] $order List of serliazed property ids to order by.
292
	 *
293
	 * @since 0.5
294
	 */
295
	public function orderByProperty( array $order = [] ) {
296
		$snaksByProperty = $this->getSnaksByProperty();
297
		$orderedProperties = array_unique( array_merge( $order, array_keys( $snaksByProperty ) ) );
298
299
		foreach ( $orderedProperties as $property ) {
300
			if ( array_key_exists( $property, $snaksByProperty ) ) {
301
				$snaks = $snaksByProperty[$property];
302
				$this->moveSnaksToBottom( $snaks );
303
			}
304
		}
305
	}
306
307
	/**
308
	 * @param Snak[] $snaks to remove and re add
309
	 */
310
	private function moveSnaksToBottom( array $snaks ) {
311
		foreach ( $snaks as $snak ) {
312
			$this->removeSnak( $snak );
313
			$this->addSnak( $snak );
314
		}
315
	}
316
317
	/**
318
	 * Gets the snaks in the current object in an array
319
	 * grouped by property id
320
	 *
321
	 * @return array[]
322
	 */
323
	private function getSnaksByProperty() {
324
		$snaksByProperty = [];
325
326
		foreach ( $this as $snak ) {
327
			/** @var Snak $snak */
328
			$propertyId = $snak->getPropertyId()->getSerialization();
329
			if ( !isset( $snaksByProperty[$propertyId] ) ) {
330
				$snaksByProperty[$propertyId] = [];
331
			}
332
			$snaksByProperty[$propertyId][] = $snak;
333
		}
334
335
		return $snaksByProperty;
336
	}
337
338
}
339